Skip to content

fix(dart-sdk): report a Parakeet stream closed before ready - #22347

Open
apoorvdarshan wants to merge 3 commits into
BasedHardware:mainfrom
apoorvdarshan:fix/dart-sdk-parakeet-refused-before-ready
Open

apoorvdarshan wants to merge 3 commits into
BasedHardware:mainfrom
apoorvdarshan:fix/dart-sdk-parakeet-refused-before-ready

Conversation

@apoorvdarshan

Copy link
Copy Markdown
Collaborator

What changed and why

Fixes #22346

ParakeetTranscriber in the Dart device SDK (sdks/device/dart) drops PCM until the server sends {"type":"ready"}. The Parakeet server can accept the WebSocket and then close it before ready: 1013 service_not_ready while the GPU worker loads, 1013 capacity_full or allocation_rejected from admission, or 1011 (stream_transcribe in backend/parakeet/main.py). The stream listener's onDone and onError only marked the stream drained, and appendPcm only checked _ready. Every later appendPcm dropped its chunk and stop() completed normally, so the caller lost the whole session's audio with no error to retry on or fall back from. #22301 fixes the same bug in the Go SDK.

  • When the stream ends on its own before ready, and before stop(), the transcriber keeps why: the channel's close code and reason, or the stream error for a failed connection.
  • From then on appendPcm throws it as a new SttStreamClosedException (closeCode, closeReason, error), and stop() throws it after it has closed the socket. A stream still waiting for ready keeps dropping PCM, and a close that answers our own finalize during stop() is not reported. Deepgram is unchanged.
  • The Dart SDK README documents the exception and how the tests run.
  • No CI job ran this package's tests: no workflow or manifest lane selects sdks/device/dart, and the Hygiene runner that executes manifest checks has no Flutter toolchain. A new path-scoped .github/workflows/sdk-dart.yml, like sdk-cpp.yml and sdk-rust.yml, sets up the pinned Flutter 3.44.5 and runs flutter test in the package. .github/ci-budget.yaml records it at 1 run/day x 5 minutes.

Product invariants affected

none

How it was verified

  • Traced the path on origin/main (0a132902c6): the ParakeetTranscriber listener, appendPcm and stop() in sdks/device/dart/lib/stt/stt.dart, and the server's closes before ready in backend/parakeet/main.py and backend/parakeet/admission.py.
  • Read web_socket_channel 3.0.3, which WebSocketChannel.connect uses: on a close frame it stores closeCode and closeReason before it closes the stream, so both are set when onDone runs, and a failed connection arrives as a WebSocketChannelException stream error followed by done.
  • Checked each existing Parakeet test against the change: the stop() tests end the fake stream only in response to finalize, after stop() has marked the transcriber stopped, so they still complete without an error.
  • python3 .github/scripts/check_ci_budget.py --base origin/main passes with the new entries.
  • dart format --line-length 120 --set-exit-if-changed (Flutter 3.44.5's Dart 3.12.2, run from the repository root like the Formatting job) reports both Dart files unchanged.
  • I did not run flutter test locally. The new Dart Device SDK workflow runs the package's suite on this PR, and the rest of CI runs with ci:full.

Tests

sdks/device/dart/test/stt_test.dart:

  • Parakeet reports a stream the server closed before ready: the fake channel closes with 1013 capacity_full before ready, as the hosted server does at capacity. A chunk appended while ready is pending is dropped without an error. After the close, appendPcm and stop() both throw SttStreamClosedException with code 1013 and reason capacity_full, no PCM is sent, and the socket is closed. On main, appendPcm returns normally and stop() completes, so the test fails.
  • Parakeet reports a stream that failed before ready: the stream errors and ends before ready; both calls throw the exception carrying that error.

Both were written to fail without the fix and run in CI; they have not been run locally.

UI evidence

UI-Evidence: none -- Dart device SDK library change with no UI; the regression tests above cover the behaviour

Failure class (fixes)

Failure-Class: new

Failure-class transition narrative

New class FC-readiness-gate-swallows-terminal-close. It is the class #22301 (open) declares for the Go SDK instance. It is not on main yet, so this PR adds a byte-identical copy of that file (blob 048aec1a3d), and whichever of the two PRs merges second adds nothing new to it. If #22301 merges first, this PR's declaration becomes Failure-Class: FC-readiness-gate-swallows-terminal-close.

  • Violated contract: a streaming client that holds back writes until the server's ready handshake must report a close that arrives before that handshake, instead of dropping every write without an error.
  • Canonical guard: the stream listener records why the stream ended before it signals completion, and the pre-ready write path and stop() report it once the stream has ended. Here Parakeet reports a stream the server closed before ready closes the stream before ready and checks both calls.
  • Evidence: issue Dart device SDK drops all audio without an error when Parakeet closes the stream before ready #22346 and stt.dart on origin/main before this change. evidence_prs is empty because neither PR has merged.

New guards

.github/workflows/sdk-dart.yml runs the Dart device SDK's tests, including the regression tests above. Instance: #13627 fixed two Dart SDK STT bugs (#13625, #13635) with tests that its PR validated only with a local flutter test run, because no CI job selects sdks/device/dart; this PR's regression tests would not run anywhere either. It is not a manifest lane because the Hygiene job that runs manifest checks has no Flutter toolchain, so, like the C++ and Rust SDKs, the package gets its own path-scoped workflow.

Scoped cleanups

With no resolved package config for sdks/device/dart, the Formatting job formats these files at the latest Dart language version and rejects stt.dart and stt_test.dart as they are on main. Running dart format --line-length 120 on them reflows some existing lines (constructor calls and list literals); those hunks are formatting only.

🤖 Generated with Claude Code

apoorvdarshan and others added 2 commits October 19, 2026 09:02
No workflow or manifest lane ran the tests in sdks/device/dart, so the
package's STT fixes were only ever tested on a contributor's machine.
The Hygiene manifest runner has no Flutter toolchain, so add a
path-scoped workflow like sdk-cpp.yml that sets up the pinned Flutter
3.44.5 and runs flutter test for the package, and record it in the CI
budget inventory.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ParakeetTranscriber drops PCM until the server sends ready. The server
can accept the socket and then close it before ready (1013
capacity_full or service_not_ready in backend/parakeet/main.py). The
stream listener only marked the stream drained, so every later
appendPcm dropped its chunk and stop() completed normally: the caller
lost the session's audio with no error to retry on or fall back from.

When the stream ends before ready and before stop(), keep why it ended
(close code and reason, or the stream error) and throw it as an
SttStreamClosedException from appendPcm and stop(). A stream that is
still waiting for ready keeps dropping PCM. This mirrors the Go SDK fix
in BasedHardware#22301.

stt.dart and stt_test.dart are reformatted with dart format
--line-length 120, which CI's Formatting job applies to them at the
latest language version because the package has no resolved config.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@apoorvdarshan apoorvdarshan added the ci:full Maintainer-granted: run full CI on this fork PR label Oct 19, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Git-on-my-level

Copy link
Copy Markdown
Member

Verdict: the production fix is correct, well-scoped, and well tested - the Dart twin of the Go fix in #22301. Two non-blocking nits on the failure-class record; the only merge gate is maintainer sign-off on the new CI workflow.

What I verified, file by file:

  • sdks/device/dart/lib/stt/stt.dart - _streamEnded records an SttStreamClosedException exactly once, only when !_ready && !_stopped, from either onDone (captures _channel.closeCode/closeReason, e.g. 1013 capacity_full) or onError (captures the stream error). appendPcm keeps dropping PCM while ready is pending but throws the recorded close once the stream ended; stop() still sends finalize and closes the socket, then throws. A close answering our own finalize during stop() is correctly not recorded (_stopped is already true), so normal shutdown is unaffected. Deepgram is behaviorally unchanged (formatting only).
  • sdks/device/dart/test/stt_test.dart - two new tests cover both paths (server close 1013 before ready -> appendPcm and stop throw with code+reason; stream error before ready -> both throw with the original error), plus the new addIncomingError harness hook. Assertions also confirm no PCM leaked to the sink and the socket was closed.
  • .github/workflows/sdk-dart.yml - mirrors the sdk-cpp.yml/sdk-rust.yml path-scoping (pull_request + main push on sdks/device/dart/**), pins Flutter 3.44.5, and adds permissions: contents: read (tighter than its siblings - good). It ran on this PR's head and flutter test passed (Actions run 39195380181).
  • .github/ci-budget.yaml - both new entries match the existing schema; the rationales (unsampled-pr-sentinel, 5-minute budget, two PRs since 2026-07-30) are consistent with the rest of the file.
  • sdks/device/dart/README.md - accurate description of the new exception and how the tests run; matches the code.
  • .github/failure-classes/FC-readiness-gate-swallows-terminal-close.json - the contract text is good, but the record is misfiled for this PR: canonical_prevention_artifact and scope_hints point only at the Go SDK, and evidence_prs is empty even though fix(go-sdk): report a Parakeet stream closed before ready #22301 and this PR are the evidence. Consider adding the Dart test to the artifact list, sdks/device/dart/lib/stt/ to scope_hints, and the PR numbers; also whether status should flip once these land.

Blast radius: confined to the Dart device SDK package - the production Omi app uses native Pigeon BLE and no in-repo code calls these APIs, so the new throws can only reach external package consumers. Cost of the CI lane is ~1 run/day at ~5 minutes.

USER IMPACT: none directly - this hardens a device-SDK package; a device maker using the Dart SDK now gets a clear error carrying the server's close code instead of silently losing a whole recording session when Parakeet is at capacity.

Still wanted: the failure-class record fixes above, and maintainer sign-off on the new CI workflow itself (new lane + third-party action subosito/flutter-action@v2), which is the only thing between this and an approval.


by AI on behalf of David — if you need David’s attention urgently, please @Git-on-my-level and escalate with need human response.

@Git-on-my-level Git-on-my-level added workflow-review Needs maintainer review for workflow, automation, hooks, or CI behavior positive-signal Automation verified a genuine fix/quality contribution docs-tooling Layer: Documentation, examples, dev tools labels Oct 19, 2026
@Git-on-my-level

Copy link
Copy Markdown
Member

Verdict: the production fix is correct and nicely scoped; nothing blocking from my side — two follow-ups below, plus the workflow-review label because this adds CI.

sdks/device/dart/lib/stt/stt.dart — the fix is right: _streamEnded records SttStreamClosedException(closeCode: _channel.closeCode, closeReason: _channel.closeReason, error: error) only when !_ready && !_stopped, so a close that answers our own finalize during stop() is not misreported; appendPcm keeps the documented drop-until-ready behavior for a still-pending stream and only throws once the stream actually ended; stop() closes the sink before rethrowing, so no socket leak on the error path. Deepgram is untouched.

sdks/device/dart/test/stt_test.dart — the two new tests (close-before-ready with 1013/capacity_full, and error-before-ready via addIncomingError) assert both appendPcm and stop throw, and that no PCM reached the socket; everything else here is dart format churn, no assertion weakened.

.github/workflows/sdk-dart.yml + .github/ci-budget.yaml — sound and consistent with sdk-cpp.yml/sdk-rust.yml: path-scoped, permissions: contents: read, 15-min timeout, pinned Flutter 3.44.5, and subosito/flutter-action@v2 is already used by mobile-app-checks.yml and repo-checks.yml. One real gap: no dart job appears in this PR's check runs, so the workflow has not actually executed on this head — the tests it exists to run have still only run on your machine. Likely fork-PR workflow approval; a maintainer may need to approve the run (or re-push) so flutter test executes before merge.

.github/failure-classes/FC-readiness-gate-swallows-terminal-close.json — the record describes the Go SDK bug, but its canonical_prevention_artifact (sdks/device/go/omidevice/stt/parakeet_test.go) does not contain that loopback test on main (#22301 is still open) and scope_hints point at the Go tree, not sdks/device/dart. Consider landing this FC with the Go PR, or pointing it at the Dart test added here so the artifact exists from day one.

sdks/device/dart/README.md — accurate: matches the actual throw behavior and how the tests run.

API note for SDK consumers: appendPcm/stop now throw where they previously returned silently — a behavior change, documented in the README, and the exception carries the retryable close code.

USER IMPACT: none for app users — Dart SDK consumers get a clear error (with the server's close code) instead of silently losing a whole session's audio when Parakeet refuses the stream.

What I'd still want as a reviewer: (1) the new Dart workflow green on this PR before merge, (2) the FC record reconciled with where its prevention test actually lands. Workflow additions (sdk-dart.yml, the ci-budget entries) are keeping the workflow-review label for CI maintainer sign-off.


by AI on behalf of David — if you need David’s attention urgently, please @Git-on-my-level and escalate with need human response.

@Git-on-my-level Git-on-my-level removed the docs-tooling Layer: Documentation, examples, dev tools label Oct 19, 2026
kodjima33 pushed a commit that referenced this pull request Oct 19, 2026
* fix(device-ts): report a Parakeet stream closed before ready

createParakeetTranscriber drops PCM until the server sends ready. The
Parakeet server can accept the socket and then close it before ready
(1013 capacity_full or service_not_ready in backend/parakeet/main.py).
The transcriber had no close handler before stop(), so appendPcm kept
returning and dropping every chunk, and stop() returned normally: the
caller lost the session's audio with no error to retry on or fall back
from.

Record the close when the socket closes before ready and before stop(),
and throw it as an SttStreamClosedError (closeCode, closeReason) from
appendPcm and stop(); stop() throws after it has torn the socket down.
A stream still waiting for ready keeps dropping PCM. This mirrors the
Go and Dart SDK fixes in #22301 and #22347.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* ci: run with ci:full

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

@kodjima33 kodjima33 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixes #22346: Parakeet transcriber silently dropped audio on a pre-ready close with no error. Good root cause + tests, CI green. Adds a new CI workflow (.github/workflows/sdk-dart.yml) and is labelled workflow-review -- leaving for Nik to sign off on the CI change before merge.

@Git-on-my-level

Copy link
Copy Markdown
Member

Verdict: the production fix is correct, tightly scoped, and mirrors the merged Go twin (#22301); this head already carries a maintainer approval. One follow-through item on the new CI workflow below — nothing blocking.

What I verified on 51f305b:

  • sdks/device/dart/lib/stt/stt.dart — _streamEnded records _closedBeforeReady only when !_ready && !_stopped, so a close that answers our own finalize during stop() is correctly not reported; stop() sets _stopped = true before sending finalize, closing that race. appendPcm keeps the documented drop-until-ready behavior and now surfaces the recorded close; stop() throws it only after the socket is closed. Deepgram is untouched apart from dart format churn.
  • The premise checks out against backend/parakeet/main.py: pre-ready closes exist at lines 471/475 (service_not_ready, 1013/1011), line 480 (admission 1013), and line 522 (stream_initialization_failed, 1011).
  • sdks/device/dart/test/stt_test.dart — the two new tests cover both end paths: close frame (1013 capacity_full, asserting closeCode/closeReason through both appendPcm and stop) and transport error (addIncomingError, asserting error identity); they also assert no PCM reached the sink.
  • .github/workflows/sdk-dart.yml — permissions: contents: read, path-scoped like sdk-cpp.yml, pinned Flutter 3.44.5, 15-minute timeout; the .github/ci-budget.yaml entries are consistent with it.
  • .github/failure-classes/FC-readiness-gate-swallows-terminal-close.json — matches the existing FC convention; its artifacts and scope point at the Go SDK. Consider also listing sdks/device/dart/test/stt_test.dart under canonical_prevention_artifact, since this PR adds the Dart-side prevention.
  • sdks/device/dart/README.md — accurate description of the new exception and of how the tests run.

CI follow-through: no "Dart Device SDK" check-run exists on this head (the app-level "Dart Analyze & Tests" is a different job and skipped), so the new suite has not actually executed in CI on this PR — expected for a fork PR that adds the workflow file itself. Please confirm the workflow's first green run on main after merge, or on the next PR touching sdks/device/dart/, before relying on it.

Blast radius: an API contract change for sdks/device/dart consumers only — appendPcm/stop now throw SttStreamClosedException where they previously dropped audio silently, documented in the README. The production Omi app (native Pigeon BLE) is unaffected.

USER IMPACT: Dart device SDK users get a clear, retryable error carrying the server's close code when transcription is refused before the session starts, instead of silently losing the whole session's audio.

Still wanted as a reviewer: the post-merge confirmation of sdk-dart.yml's first green run, and the FC artifact addition above. Neither blocks.

— automated review on behalf of the maintainers


by AI on behalf of David — if you need David’s attention urgently, please @Git-on-my-level and escalate with need human response.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci:full Maintainer-granted: run full CI on this fork PR positive-signal Automation verified a genuine fix/quality contribution workflow-review Needs maintainer review for workflow, automation, hooks, or CI behavior

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Dart device SDK drops all audio without an error when Parakeet closes the stream before ready

3 participants