Conversation
movetz
approved these changes
Apr 29, 2026
|
Thank you so much, this solved my problem with randomly hanging tests where I run hundreds of tools quickly. |
The Client's message handling loop uses `repeat { ... } while true` around
the `for try await data in stream` loop. When the transport stream finishes
(subprocess death, pipe EOF), the for-await exits normally, but the repeat
loop immediately re-enters `connection.receive()`, gets an instantly-finishing
stream, and spins at 100% CPU indefinitely.
Server.swift does not have this bug — it uses a flat `do { for try await }`
without an outer repeat. Match that pattern in Client.
The `catch` for `isResourceTemporarilyUnavailable` (with `continue` back into
the repeat) was the original motivation for the outer loop, but that error
cannot occur on a finished stream — it only matters during active I/O.
Removing the retry-on-EAGAIN is safe because the stream itself handles
transient read errors internally.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
piersdd
force-pushed
the
fix/client-spin-on-eof
branch
from
August 23, 2026 00:58
1ea8365 to
42c30a1
Compare
Author
This was referenced Sep 10, 2026
ianegordon
added a commit
to ianegordon/swift-sdk
that referenced
this pull request
Sep 10, 2026
… entries 5 and 5a Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017DMgYCJ9tYavK3NhBCDTsn
kattgrom-hub
approved these changes
Sep 11, 2026
ianegordon
added a commit
to ianegordon/swift-sdk
that referenced
this pull request
Sep 12, 2026
…odelcontextprotocol#275 commit 8e36cfa, robertoscipionecom) Manifest entry 10. Upstream commit 8e36cfa extracted from refs/pull/275/head, unmodified: same author, same patch-id (e588896c…), cherry-picked with -x so the commit records its own provenance. Client.send suspended on a checked continuation only a matching response could resume, and the request task was unstructured, so cancelling the caller never reached it: a caller that gave up stayed suspended until the server answered, which for a dead server is never. Three linked fixes — forward cancellation from RequestContext.value to the request task, call cancelRequest from the task cancellation handler, and remember ids cancelled before their continuation registered. Two of modelcontextprotocol#275's three commits are deliberately skipped, not adapted: 40c5951 (loop termination) and fded08a (its test) duplicate manifest entries 5 and 5a from modelcontextprotocol#221. Taking them would conflict in Client.swift and produce a duplicate testMessageLoopStopsWhenStreamFinishes declaration that does not compile. The full PR head stays fetchable as branch pr/275. Two defects in this commit are carried as accepted, not fixed; see FORK.md entry 10. A cancellation landing before the send completes is notified before the request is sent, so the peer can ignore an unknown-id cancellation and then execute the request. And cancelledBeforeRegistration cannot distinguish 'not registered yet' from 'already completed', so cancelling a finished request leaves an entry until disconnect. Both need the request-state redesign that belongs upstream, not a fork patch. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Eb9yGSXH1TVkg5Afsu9phk
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
When a transport stream finishes (e.g. subprocess death closes the pipe), the Client's message handling loop spins at 100% CPU indefinitely.
The root cause is the
repeat { ... } while trueouter loop inClient.connect(transport:)(line 217). When thefor try await data in streamloop exits normally on stream EOF, therepeatre-entersconnection.receive(), gets an instantly-finishing stream, and loops again — consuming a full CPU core.The
Task.isCancelledcheck at the top of therepeatbody only helps ifdisconnect()was called externally. On a normal subprocess/transport death, no cancellation is signalled — the stream simply finishes.Server.swiftin this same package does not have this bug — it uses a flatdo { for try await ... }without an outerrepeatloop.Fix
Remove the
repeat { ... } while truewrapper, matching theServer.swiftpattern. The message handling task now:connection.receive()oncefor try awaitThe
catchforisResourceTemporarilyUnavailable(which usedcontinueto re-enter the repeat) is also removed. That EAGAIN-style error is a transient I/O condition handled internally by the transport's read loop — it does not surface through theAsyncThrowingStreamthatreceive()returns.Reproduction
StdioTransportto a subprocesssampleor Activity MonitorThe tight loop shows up as:
Test plan
disconnect()still terminates the loop correctly (cancels the task)