fix(code): keep MCP shutdown-race traceback off the terminal - #5325
Merged
Conversation
Quitting dcode while an HTTP MCP response is in flight races the
streamable-HTTP transport's response task: the session stream is already
closed, send() raises anyio.ClosedResourceError, and the MCP SDK logs
logger.exception("Error parsing JSON response"). With no handler on the
mcp logger hierarchy, the traceback clears logging.lastResort's WARNING
threshold and prints over the terminal.
Add "mcp" to _QUIET_SDK_LOGGER_NAMES so MCP diagnostics route to the
debug log when DEEPAGENTS_CODE_DEBUG is set and are swallowed via
NullHandler otherwise, matching the existing genai-prices/langchain/
langsmith treatment. Pin the coupling with a test that reads the logger
name off the installed mcp.client.streamable_http module.
The shutdown-race filter matched only `Error parsing JSON response` on `mcp.client.streamable_http`, which misses the common case. A server that streams its response answers through `_handle_sse_event`, whose `try` also wraps the read-stream `send` and logs `Error parsing SSE message`; the plain SSE transport races the same way in `sse_reader`. Drive the match from a per-logger message map so all three are covered, and keep `Error in post_writer` out of it -- that one wraps the whole write loop, so a closed stream there can mean the transport was orphaned mid-session. Require *every* leaf of a `BaseExceptionGroup` to be a stream teardown before dropping the record. Matching on any leaf suppressed groups that also carried a real fault, which was the only actionable thing in them. Resolve anyio's exception types through a cached helper instead of importing inside the filter body. `logging` swallows exceptions from `Handler.emit` but not from `Logger.filter`, so an `ImportError` there would have propagated into the transport's `except` block; the guarded import degrades to keeping the record, and caching keeps `anyio` off the startup path. Correct the docstrings. The dropped record is not redundant because the transport re-sends the exception -- that `send` is what raised, so the retry raises too. What actually surfaces the failure is the session receive loop handing pending streams an `ErrorData(CONNECTION_CLOSED)`. Tests: restore the transport loggers' filters via `monkeypatch.setattr` rather than leaving the process-global loggers stripped, pin that `mcp` never enters `_QUIET_SDK_LOGGER_NAMES` (a `NullHandler` there would skip `lastResort` and hide every transport diagnostic), and add an end-to-end case that logs through the real logger so a filter on the wrong target fails.
Mason Daugherty (mdrxy)
pushed a commit
that referenced
this pull request
Aug 6, 2026
> [!CAUTION] > Merging this PR will automatically publish to **PyPI** and create a **GitHub release**. For the full release process, see [`.github/RELEASING.md`](https://github.com/langchain-ai/deepagents/blob/main/.github/RELEASING.md). --- _Release notes preview: keep this section in sync with the package `CHANGELOG.md`. Publish reads the merged CHANGELOG via `release.yml`, not this PR description — keep them aligned anyway so the PR stays an accurate historical record for reviewers and anyone returning later._ --- ## [0.1.53](deepagents-code==0.1.52...deepagents-code==0.1.53) (2026-08-06) ### Features - Added pricing coverage with Baseten built-in overrides and local fallback overrides when `genai-prices` is missing data ([#5312](#5312), [#5304](#5304)). - Suggest compacting large resumed threads ([#5318](#5318)). - Added terminal program trace metadata ([#5329](#5329)). ### Bug Fixes - Preserved runtime offload archive routing ([#5328](#5328)). - Always restart after a successful startup auto-update ([#5317](#5317)). - Fixed leaked turn coroutines and SQLite handles ([#5218](#5218)). - Keep MCP shutdown-race tracebacks from appearing in the terminal ([#5325](#5325)). - Open the `/auto model` selector immediately while connecting ([#5341](#5341)). - Route failures to `PostToolUseFailure` ([#5315](#5315)). - Use dismissed copy for ask-user prompts ([#5331](#5331)). _End release notes preview._ --- > [!NOTE] > A **New Contributors** section is appended to the GitHub release notes automatically at publish time (see [Release Pipeline](https://github.com/langchain-ai/deepagents/blob/main/.github/RELEASING.md#release-pipeline), step 2). --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: langchain-oss-automated-triage[bot] <248757908+langchain-oss-automated-triage[bot]@users.noreply.github.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.
Quitting
dcodewhile an MCP response is in flight no longer prints ananyio.ClosedResourceErrortraceback to the terminal on exit.An MCP transport's reader task can still be running while the session stream is torn down. Its
read_stream_writer.send(...)raisesanyio.ClosedResourceError, the SDK's surroundingexceptcallslogger.exception(...), and since nothing handles themcphierarchy the record clearslogging.lastResort's WARNING threshold and prints a full traceback — alarming-looking but harmless, since the app was already exiting.A
logging.Filteron each transport's own logger drops those records. It has to live on the emitting logger: logger-level filters never run for records propagated up from children._MCP_SHUTDOWN_RACE_MESSAGESlists the messages whosetryblock wraps a read-streamsend, and a record is dropped only if it carries an anyio closed/broken-resource error:mcp.client.streamable_httpError parsing JSON response(single JSON body)mcp.client.streamable_httpError parsing SSE message(streamed response)mcp.client.sseError in sse_readerError in post_writeris excluded — it wraps the entire write loop, so a closed stream there can also mean the transport was orphaned while the session was live.Why this is safe. The in-flight request still fails loudly: the session's receive loop hands pending response streams an
ErrorData(CONNECTION_CLOSED)on its way out, sosend_requestraisesMcpError("Connection closed"). A network-level drop can't reach the filter —httpcoreremaps anyio's stream errors ontohttpx.ReadErrorand friends before httpx re-raises, and the filter never walks__cause__. For aBaseExceptionGroup, every leaf must be a teardown, so a group carrying a real fault stays visible. Themcphierarchy gets noNullHandler(pinned by a test), so everything else reaches stderr.Debug mode. With
DEEPAGENTS_CODE_DEBUGset, an installed filter is removed and the transport loggers are routed to the debug log. Both halves matter: the filter would otherwise suppress the record for the debug file handler too, and without that handler it would fall through tolastResortand print over the TUI.Drift-pin tests read the logger names off the installed
mcpmodules — mirroring the existinggenai-pricespin — so an upstream rename fails in CI rather than in a user's terminal.