fix(mcp): swallow broken/closed-pipe on dg mcp startup notifications and error path - #88
Conversation
…and error path When `dg mcp` runs as an MCP server and the host closes stdio, the startup update-notification write to stderr raised BrokenPipeError, which then cascaded into an unhandled ValueError from rich while the error handler tried to print to the already-closed console — crashing through the excepthook and generating a Sentry event (DX-CLI-P). - Guard all three startup notification writers (update, plugin, skills) against (BrokenPipeError, OSError, ValueError), matching the existing closed-stream pattern in base_command.py. - Route main()'s error/interrupt handlers through a new _safe_console_print() helper so a closed console no longer crashes the process on exit. - Defense-in-depth: drop broken-pipe / closed-file events in the Sentry before_send hook (_is_broken_pipe) so residual pipe-close on dg mcp produces no noise. Adds unit coverage for the notification guard and the Sentry filter. Sentry: https://deepgram.sentry.io/issues/DX-CLI-P Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds the missing regression tests for the three DX-CLI-P guards that shipped without coverage: - _safe_console_print (main.py): swallows BrokenPipeError / OSError / rich's closed-file ValueError, plus an end-to-end test that main() still exits(2) when the cli raises AND the console is closed (the exact crash cascade). - skills print_pending_notification: tolerates a broken stderr. - plugin print_pending_plugin_notifications: tolerates a broken stderr. Each guard test fails if its try/except is removed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Reviewer context — DX-CLI-PWhat broke: when The fix (commit 6091c6a):
Coverage (commit 5b9ea16): added regression tests for all three guards, including an end-to-end test that reproduces the exact cascade — cli raises and the console is closed — and asserts Verification:
One earlier review nit (bare |
🤖 I have created a release *beep* *boop* --- ## 📦 deepctl 0.2.27 — what's in this release Ships the Deepgram Python SDK **7.7.0** upgrade, makes **Flux** the default text-to-speech model, expands redaction/formatting controls for speech-to-text, and hardens the MCP proxy against host disconnects. All feature areas were exercised end-to-end against the live API before cutting this release. ### ✨ Features - **`dg speak` now defaults to Flux TTS (`flux-alexis-en`).** Text-to-speech streams via Flux (Speak v2 WebSocket) by default instead of Aura 2, with new Flux-only controls: `--speed` (0.85–1.15) and `--expressivity` (−2…2, beta). Aura voices remain available via `-m aura-2-*` (REST batch). (#89, #92) - **`dg listen` redaction & numerals on files *and* live streams.** New `--redact` and `--numerals`. Flux STT (v2) accepts `--redact numbers` / `aggressive_numbers`; v1 models also accept `pci`, `ssn`, etc. and are repeatable. (#92) - **Flux STT streaming guardrail.** Flux STT is streaming-only — a file or URL source now fails fast with guidance to use `--mic`, stdin (`-`), or a v1 model (e.g. `nova-3`) for pre-recorded audio, instead of erroring opaquely. (#92) - **Deepgram SDK bumped to 7.7.0.** (#92) ### 🐛 Bug Fixes - **MCP pipe-safety.** `dg mcp` now swallows broken/closed-pipe errors during startup notifications and on the error path, so a host disconnect exits cleanly with no Python traceback. (#88) ### 🔧 Internal - Test isolation: the output-format global no longer leaks across the test suite. (#93) ###⚠️ Upgrade note `dg speak` output now comes from Flux (`flux-alexis-en`) by default rather than Aura 2 — expect a different default voice and raw `linear16` (auto-wrapped to WAV) instead of Aura's MP3 default. To keep the previous behavior, pass `-m aura-2-asteria-en` (add `--encoding mp3` for MP3 output). --- <sub>The machine-generated release manifest below is required by Release Please and is left unchanged.</sub> <details><summary>0.2.27</summary> ## [0.2.27](v0.2.26...v0.2.27) (2026-08-17) ### Features * SDK 7.7.0 — Flux TTS controls, Flux STT fix, listen redact/numerals ([#92](#92)) ([50d96cf](50d96cf)) * **speak:** default to Flux TTS (flux-alexis-en) instead of Aura 2 ([#89](#89)) ([5a0b698](5a0b698)) ### Bug Fixes * **mcp:** swallow broken/closed-pipe on dg mcp startup notifications and error path ([#88](#88)) ([b24396e](b24396e)) </details> <details><summary>deepctl-core: 0.2.15</summary> ## [0.2.15](deepctl-core-v0.2.14...deepctl-core-v0.2.15) (2026-08-17) ### Features * SDK 7.7.0 — Flux TTS controls, Flux STT fix, listen redact/numerals ([#92](#92)) ([50d96cf](50d96cf)) </details> <details><summary>deepctl-telemetry: 0.0.6</summary> ## [0.0.6](deepctl-telemetry-v0.0.5...deepctl-telemetry-v0.0.6) (2026-08-17) ### Bug Fixes * **mcp:** swallow broken/closed-pipe on dg mcp startup notifications and error path ([#88](#88)) ([b24396e](b24396e)) </details> <details><summary>deepctl-cmd-login: 0.1.17</summary> ## [0.1.17](deepctl-cmd-login-v0.1.16...deepctl-cmd-login-v0.1.17) (2026-08-17) ### Features * SDK 7.7.0 — Flux TTS controls, Flux STT fix, listen redact/numerals ([#92](#92)) ([50d96cf](50d96cf)) </details> <details><summary>deepctl-cmd-projects: 0.1.13</summary> ## [0.1.13](deepctl-cmd-projects-v0.1.12...deepctl-cmd-projects-v0.1.13) (2026-08-17) ### Features * SDK 7.7.0 — Flux TTS controls, Flux STT fix, listen redact/numerals ([#92](#92)) ([50d96cf](50d96cf)) </details> <details><summary>deepctl-cmd-usage: 0.1.13</summary> ## [0.1.13](deepctl-cmd-usage-v0.1.12...deepctl-cmd-usage-v0.1.13) (2026-08-17) ### Features * SDK 7.7.0 — Flux TTS controls, Flux STT fix, listen redact/numerals ([#92](#92)) ([50d96cf](50d96cf)) </details> <details><summary>deepctl-cmd-update: 0.2.6</summary> ## [0.2.6](deepctl-cmd-update-v0.2.5...deepctl-cmd-update-v0.2.6) (2026-08-17) ### Bug Fixes * **mcp:** swallow broken/closed-pipe on dg mcp startup notifications and error path ([#88](#88)) ([b24396e](b24396e)) </details> <details><summary>deepctl-cmd-skills: 0.0.7</summary> ## [0.0.7](deepctl-cmd-skills-v0.0.6...deepctl-cmd-skills-v0.0.7) (2026-08-17) ### Bug Fixes * **mcp:** swallow broken/closed-pipe on dg mcp startup notifications and error path ([#88](#88)) ([b24396e](b24396e)) </details> <details><summary>deepctl-cmd-speak: 0.0.4</summary> ## [0.0.4](deepctl-cmd-speak-v0.0.3...deepctl-cmd-speak-v0.0.4) (2026-08-17) ### Features * SDK 7.7.0 — Flux TTS controls, Flux STT fix, listen redact/numerals ([#92](#92)) ([50d96cf](50d96cf)) * **speak:** default to Flux TTS (flux-alexis-en) instead of Aura 2 ([#89](#89)) ([5a0b698](5a0b698)) </details> <details><summary>deepctl-cmd-listen: 0.0.14</summary> ## [0.0.14](deepctl-cmd-listen-v0.0.13...deepctl-cmd-listen-v0.0.14) (2026-08-17) ### Features * SDK 7.7.0 — Flux TTS controls, Flux STT fix, listen redact/numerals ([#92](#92)) ([50d96cf](50d96cf)) </details> --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please). Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
🤖 I have created a release *beep* *boop* --- <details><summary>0.3.0</summary> ## [0.3.0](v0.2.27...v0.3.0) (2026-08-19) ### ⚠ BREAKING CHANGES * `dg` now exits non-zero when a command fails: 1 for errors (including crashes and usage errors), 2 for user interrupt, 0 on success. Every command previously exited 0 regardless of outcome, so scripts and CI steps that ignored the exit code will surface failures they were silently swallowing. No command that succeeds changes its exit code. ### Bug Fixes * correct web command examples, document Flux TTS/STT, and honor -o json across account commands ([#97](#97)) ([55984ec](55984ec)) * dependency floors that let dg update skip this release, and exit-code + error-stream correctness ([#102](#102)) ([fd1e8a4](fd1e8a4)) * **deps:** cap mcp <2 (fixes broken dg mcp), commit uv.lock, require twine >=7 ([#95](#95)) ([997cd36](997cd36)) * **deps:** raise deepctl-core floor to 0.2.16 in the eight packages that import get_status_console ([98f9e91](98f9e91)) * **deps:** raise root dependency floors to the versions this release publishes ([c0b0023](c0b0023)) * exit 1, not 2, when a command crashes or is misused ([914e132](914e132)) * keep exit 2 when Ctrl-C interrupts a running command ([b0e80e2](b0e80e2)) * **keys:** honor -o json so stdout stays parseable (completes the [#97](#97) sweep) ([#101](#101)) ([e430a77](e430a77)) * **release:** bump pypi-publish action to v1.14.2 for Metadata-Version 2.5 ([#94](#94)) ([582cd83](582cd83)) * send root error and interrupt output to stderr, not stdout ([f4b7c48](f4b7c48)) * **web:** repair broken Heap snippet, upgrade astro 6→7, clear all 20 npm alerts ([#96](#96)) ([11928fe](11928fe)) ### Behavior changes Alongside the exit-code change above, upgrading to 0.3.0 changes these: * The full exit-code contract is now enforced end to end: `0` = success, `1` = error, `2` = user interrupt. Crashes **and usage errors** (bad flag, unknown command, bare `dg`) exit `1`; `2` is reserved for cancellation, so Ctrl-C during a running command and Ctrl-D at a prompt both still exit `2`. * Error and cancellation messages are written to **stderr** instead of stdout. `dg -o json …` therefore keeps stdout machine-readable when a command fails — previously a failure printed `Error: …` prose to stdout, so a script piping stdout into `jq` parsed the error text instead of JSON. Successful commands still write their payload to stdout. * `-o yaml` and `-o csv` no longer drop square-bracketed text from values. Output was passed through a renderer that read `[...]` as style markup and deleted it, so an API key comment of `[ci] runner` was emitted as `runner`. Long values are also no longer hard-wrapped mid-field. * `dg keys --delete KEY_ID` now asks for confirmation on stderr instead of always reporting `Cancelled by user` without deleting. In a non-interactive context it exits `1` and tells you to pass `--yes`. * `dg keys --create --dry-run` now reports what it would create. It previously failed with an internal `TypeError`. ### Previously unreleased 0.2.27 was tagged on 2026-08-17 but never reached PyPI — its publish step failed with `InvalidDistribution: Invalid distribution metadata: '2.5' is not a valid metadata version`, which [#94](#94) and [#95](#95) then fixed. PyPI therefore goes straight from 0.2.26 to 0.3.0, and this release is the first published build to include the 0.2.27 changes: * SDK 7.7.0 — Flux TTS controls, Flux STT fix, listen redact/numerals ([#92](#92)) ([50d96cf](50d96cf)) * **speak:** default to Flux TTS (`flux-alexis-en`) instead of Aura 2 ([#89](#89)) ([5a0b698](5a0b698)). This changes the default model for `dg speak`, so synthesised audio differs unless you pass an `aura-*` model explicitly. * **mcp:** swallow broken/closed-pipe on dg mcp startup notifications and error path ([#88](#88)) ([b24396e](b24396e)) Six packages tagged in that cycle also reach PyPI for the first time here: `deepctl-cmd-listen` 0.0.14, `deepctl-cmd-login` 0.1.17, `deepctl-cmd-skills` 0.0.7, `deepctl-cmd-speak` 0.0.4, `deepctl-cmd-update` 0.2.6 and `deepctl-telemetry` 0.0.6. Because 0.2.27 never published, `dg update` on pip also had to be repaired for this release to arrive at all: root's inter-package dependency floors were lower than the versions being published, so pip's default `only-if-needed` strategy left most sub-packages stale and `dg --version` reported the new number while the fixes never landed. Floors now match the published versions exactly. </details> <details><summary>deepctl-core: 0.2.16</summary> ## [0.2.16](deepctl-core-v0.2.15...deepctl-core-v0.2.16) (2026-08-19) ### Bug Fixes * correct web command examples, document Flux TTS/STT, and honor -o json across account commands ([#97](#97)) ([55984ec](55984ec)) * **keys:** honor -o json so stdout stays parseable (completes the [#97](#97) sweep) ([#101](#101)) ([e430a77](e430a77)) ### Behavior changes * Commands now map their result status to a process exit code (`error` → `1`, `cancelled` → `2`, otherwise `0`), and `BaseCommand.exit_code_for()` exposes that mapping. Exit codes were previously discarded, so every command exited `0`. * `-o yaml` and `-o csv` payloads are written verbatim; the renderer no longer interprets `[...]` as markup or wraps long values. * New `get_status_console()` returns the shared stderr console for status output. Commands should use it instead of declaring their own. Packages that import it require `deepctl-core>=0.2.16`. </details> <details><summary>deepctl-cmd-projects: 0.2.0</summary> ## [0.2.0](deepctl-cmd-projects-v0.1.13...deepctl-cmd-projects-v0.2.0) (2026-08-19) ### ⚠ BREAKING CHANGES * `dg` now exits non-zero when a command fails: 1 for errors (including crashes and usage errors), 2 for user interrupt, 0 on success. Every command previously exited 0 regardless of outcome, so scripts and CI steps that ignored the exit code will surface failures they were silently swallowing. No command that succeeds changes its exit code. ### Bug Fixes * correct web command examples, document Flux TTS/STT, and honor -o json across account commands ([#97](#97)) ([55984ec](55984ec)) * dependency floors that let dg update skip this release, and exit-code + error-stream correctness ([#102](#102)) ([fd1e8a4](fd1e8a4)) * **deps:** raise deepctl-core floor to 0.2.16 in the eight packages that import get_status_console ([98f9e91](98f9e91)) * **keys:** honor -o json so stdout stays parseable (completes the [#97](#97) sweep) ([#101](#101)) ([e430a77](e430a77)) </details> <details><summary>deepctl-cmd-usage: 0.2.0</summary> ## [0.2.0](deepctl-cmd-usage-v0.1.13...deepctl-cmd-usage-v0.2.0) (2026-08-19) ### ⚠ BREAKING CHANGES * `dg` now exits non-zero when a command fails: 1 for errors (including crashes and usage errors), 2 for user interrupt, 0 on success. Every command previously exited 0 regardless of outcome, so scripts and CI steps that ignored the exit code will surface failures they were silently swallowing. No command that succeeds changes its exit code. ### Bug Fixes * correct web command examples, document Flux TTS/STT, and honor -o json across account commands ([#97](#97)) ([55984ec](55984ec)) * dependency floors that let dg update skip this release, and exit-code + error-stream correctness ([#102](#102)) ([fd1e8a4](fd1e8a4)) * **deps:** raise deepctl-core floor to 0.2.16 in the eight packages that import get_status_console ([98f9e91](98f9e91)) * **keys:** honor -o json so stdout stays parseable (completes the [#97](#97) sweep) ([#101](#101)) ([e430a77](e430a77)) </details> <details><summary>deepctl-cmd-mcp: 0.1.15</summary> ## [0.1.15](deepctl-cmd-mcp-v0.1.14...deepctl-cmd-mcp-v0.1.15) (2026-08-19) ### Bug Fixes * **deps:** cap mcp <2 (fixes broken dg mcp), commit uv.lock, require twine >=7 ([#95](#95)) ([997cd36](997cd36)) </details> <details><summary>deepctl-cmd-models: 0.1.0</summary> ## [0.1.0](deepctl-cmd-models-v0.0.2...deepctl-cmd-models-v0.1.0) (2026-08-19) ### ⚠ BREAKING CHANGES * `dg` now exits non-zero when a command fails: 1 for errors (including crashes and usage errors), 2 for user interrupt, 0 on success. Every command previously exited 0 regardless of outcome, so scripts and CI steps that ignored the exit code will surface failures they were silently swallowing. No command that succeeds changes its exit code. ### Bug Fixes * correct web command examples, document Flux TTS/STT, and honor -o json across account commands ([#97](#97)) ([55984ec](55984ec)) * dependency floors that let dg update skip this release, and exit-code + error-stream correctness ([#102](#102)) ([fd1e8a4](fd1e8a4)) * **deps:** raise deepctl-core floor to 0.2.16 in the eight packages that import get_status_console ([98f9e91](98f9e91)) * **keys:** honor -o json so stdout stays parseable (completes the [#97](#97) sweep) ([#101](#101)) ([e430a77](e430a77)) </details> <details><summary>deepctl-cmd-keys: 0.1.0</summary> ## [0.1.0](deepctl-cmd-keys-v0.0.3...deepctl-cmd-keys-v0.1.0) (2026-08-19) ### ⚠ BREAKING CHANGES * `dg` now exits non-zero when a command fails: 1 for errors (including crashes and usage errors), 2 for user interrupt, 0 on success. Every command previously exited 0 regardless of outcome, so scripts and CI steps that ignored the exit code will surface failures they were silently swallowing. No command that succeeds changes its exit code. ### Bug Fixes * dependency floors that let dg update skip this release, and exit-code + error-stream correctness ([#102](#102)) ([fd1e8a4](fd1e8a4)) * **deps:** raise deepctl-core floor to 0.2.16 in the eight packages that import get_status_console ([98f9e91](98f9e91)) * **keys:** honor -o json so stdout stays parseable (completes the [#97](#97) sweep) ([#101](#101)) ([e430a77](e430a77)) </details> <details><summary>deepctl-cmd-read: 0.1.0</summary> ## [0.1.0](deepctl-cmd-read-v0.0.2...deepctl-cmd-read-v0.1.0) (2026-08-19) ### ⚠ BREAKING CHANGES * `dg` now exits non-zero when a command fails: 1 for errors (including crashes and usage errors), 2 for user interrupt, 0 on success. Every command previously exited 0 regardless of outcome, so scripts and CI steps that ignored the exit code will surface failures they were silently swallowing. No command that succeeds changes its exit code. ### Bug Fixes * correct web command examples, document Flux TTS/STT, and honor -o json across account commands ([#97](#97)) ([55984ec](55984ec)) * dependency floors that let dg update skip this release, and exit-code + error-stream correctness ([#102](#102)) ([fd1e8a4](fd1e8a4)) * **deps:** raise deepctl-core floor to 0.2.16 in the eight packages that import get_status_console ([98f9e91](98f9e91)) * **keys:** honor -o json so stdout stays parseable (completes the [#97](#97) sweep) ([#101](#101)) ([e430a77](e430a77)) </details> <details><summary>deepctl-cmd-requests: 0.1.0</summary> ## [0.1.0](deepctl-cmd-requests-v0.0.2...deepctl-cmd-requests-v0.1.0) (2026-08-19) ### ⚠ BREAKING CHANGES * `dg` now exits non-zero when a command fails: 1 for errors (including crashes and usage errors), 2 for user interrupt, 0 on success. Every command previously exited 0 regardless of outcome, so scripts and CI steps that ignored the exit code will surface failures they were silently swallowing. No command that succeeds changes its exit code. ### Bug Fixes * correct web command examples, document Flux TTS/STT, and honor -o json across account commands ([#97](#97)) ([55984ec](55984ec)) * dependency floors that let dg update skip this release, and exit-code + error-stream correctness ([#102](#102)) ([fd1e8a4](fd1e8a4)) * **deps:** raise deepctl-core floor to 0.2.16 in the eight packages that import get_status_console ([98f9e91](98f9e91)) * **keys:** honor -o json so stdout stays parseable (completes the [#97](#97) sweep) ([#101](#101)) ([e430a77](e430a77)) </details> <details><summary>deepctl-cmd-billing: 0.1.0</summary> ## [0.1.0](deepctl-cmd-billing-v0.0.2...deepctl-cmd-billing-v0.1.0) (2026-08-19) ### ⚠ BREAKING CHANGES * `dg` now exits non-zero when a command fails: 1 for errors (including crashes and usage errors), 2 for user interrupt, 0 on success. Every command previously exited 0 regardless of outcome, so scripts and CI steps that ignored the exit code will surface failures they were silently swallowing. No command that succeeds changes its exit code. ### Bug Fixes * correct web command examples, document Flux TTS/STT, and honor -o json across account commands ([#97](#97)) ([55984ec](55984ec)) * dependency floors that let dg update skip this release, and exit-code + error-stream correctness ([#102](#102)) ([fd1e8a4](fd1e8a4)) * **deps:** raise deepctl-core floor to 0.2.16 in the eight packages that import get_status_console ([98f9e91](98f9e91)) * **keys:** honor -o json so stdout stays parseable (completes the [#97](#97) sweep) ([#101](#101)) ([e430a77](e430a77)) </details> <details><summary>deepctl-cmd-members: 0.1.0</summary> ## [0.1.0](deepctl-cmd-members-v0.0.3...deepctl-cmd-members-v0.1.0) (2026-08-19) ### ⚠ BREAKING CHANGES * `dg` now exits non-zero when a command fails: 1 for errors (including crashes and usage errors), 2 for user interrupt, 0 on success. Every command previously exited 0 regardless of outcome, so scripts and CI steps that ignored the exit code will surface failures they were silently swallowing. No command that succeeds changes its exit code. ### Bug Fixes * correct web command examples, document Flux TTS/STT, and honor -o json across account commands ([#97](#97)) ([55984ec](55984ec)) * dependency floors that let dg update skip this release, and exit-code + error-stream correctness ([#102](#102)) ([fd1e8a4](fd1e8a4)) * **deps:** raise deepctl-core floor to 0.2.16 in the eight packages that import get_status_console ([98f9e91](98f9e91)) * **keys:** honor -o json so stdout stays parseable (completes the [#97](#97) sweep) ([#101](#101)) ([e430a77](e430a77)) </details> --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please).
Summary
Fixes DX-CLI-P —
dg mcpcrashes withValueError: I/O operation on closed fileon a broken stderr pipe.When
dgruns as an MCP server and the host closes stdio (a normal condition on client disconnect), the crash was a two-stage cascade:print_pending_notification()does a rawsys.stderr.write(...). On a closed pipe this raisesBrokenPipeError.except Exceptionhandler inmain()then calledconsole.print(...), but the console's underlying stream is also closed, so rich raisedValueError: I/O operation on closed file— an unhandled exception that escaped to the excepthook, crashing the process and generating a Sentry event.Changes
(BrokenPipeError, OSError, ValueError), matching the existing closed-stream pattern inbase_command.py.main()'s error path stream-safe via a new_safe_console_print()helper, so a closed console no longer crashes the process on exit. Both theKeyboardInterruptand genericExceptionhandlers route through it.before_sendhook (_is_broken_pipe) so residual pipe-close ondg mcpproduces no noise.Testing
test_telemetry.py+test_startup_check.py: 42 passed (incl. 6 new)ruff check/ruff format --check/mypyon changed files: cleanprint_pending_notification()(survives without raising) and ran a realdg mcpprocess with a broken output pipe (exit 0, no traceback leaked).Sentry: https://deepgram.sentry.io/issues/DX-CLI-P
🤖 Generated with Claude Code