Build on the Swift 6.2 toolchains the README promises - #162
Merged
Conversation
`swift build -c release` failed on every Swift 6.2 toolchain — Xcode 26.0 through 26.3 — with three `#SendingRisksDataRace` errors in ServerInference.swift. Nobody outside Xcode 26.4+ has been able to build since the image-support merge in 0.5.0, against a README that promises Swift 6.2 or newer. Reported as issue 161. CI never caught it: `runs-on: macos-26` resolves to the newest Xcode on the image, currently 26.6. Reproduced against a real Swift 6.2.4 compiler. `ServerModelSession` is an actor, so its `visionRuntime` and `model` are self-isolated, and both were captured by non-escaping closures handed to the nonisolated helpers `ServerRequestImages.encode` and `.encodeAll`. Swift 6.2's region isolation cannot prove those closures stay in the actor's domain and calls the capture a send; 6.3 accepts the same code. Nothing escapes: the helpers are synchronous, the closures are non-escaping, and every call runs on the actor. The closures are what has to go, so the decisions become values. `ServerRequestImages.source(for:)` returns which read an encode does, and `plannedEntries` takes the dictionary snapshot, the plan pass and the id-to-plan pairing together — `[UUID: URL]` has no order, so pairing ids and plans from two walks files one image's features under another's id, and when both project to the same soft-token count nothing downstream notices. Along the way: - A truncated upload now answers 400 `invalid_image` instead of 500. Admission reads dimensions with stream verification off and the encode re-plans with it on, so a truncated image is admitted and fails the second read; that failure reached the generic handler as a server error, which official clients retry and 4xx they do not. - The plans are released before the render walks the whole history rather than after, so up to 32 ImageIO descriptors are not held across it. - Two 6.2-only test-target breaks: `channel.getOption` resolving to the `EventLoopFuture` overload, and `Attachment.record(_ image: NSImage, as:)`, which the Testing library only gained in 6.3. - A `build-oldest-supported-toolchain` CI job selecting Xcode 26.3, so this cannot regress silently again. Build only: running the suite twice would double CI time. Verified: release build on Swift 6.2.4 and 6.3.3; 128 tests green; both rewritten test cases mutation-checked by reverting the fix and watching them fail; and five real multimodal requests A/B'd against main — four byte-identical in reply and token counts, the fifth the intended 500 → 400.
drumih
force-pushed
the
fix/swift-62-build
branch
from
August 28, 2026 10:11
75460fb to
3230e08
Compare
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.
Fixes #161.
What was broken
swift build -c releasefailed on every Swift 6.2 toolchain — Xcode 26.0, 26.1, 26.2, 26.3 — with three#SendingRisksDataRaceerrors inServerInference.swift. Nobody outside Xcode 26.4+ has been able to build since the image-support merge in 0.5.0, against a README that promises "Xcode 26 and Swift 6.2 or newer".CI never caught it:
runs-on: macos-26resolves to the newest Xcode on the image, currently 26.6 / Swift 6.3.3.Cause
ServerModelSessionis an actor, sovisionRuntimeandmodelare self-isolated. Both were captured by non-escaping closures handed to the nonisolated helpersServerRequestImages.encodeand.encodeAll. Swift 6.2's region isolation cannot prove those closures stay in the actor's isolation domain and reports the capture as a send; Swift 6.3 accepts the same code.Nothing actually escapes — the helpers are synchronous, the closures are non-escaping, and every call runs on the actor. Verified against a real Swift 6.2.4 compiler, with a 25-line reproducer that 6.2.4 rejects and 6.3.3 accepts. An
isolatedparameter, aSendableconstraint, dropping the generics, and folding the two closures into one all still fail;nonisolated(unsafe)fixes only two of the three sites.Fix
The closures are what has to go, so the decisions become values:
ServerRequestImages.source(for:)returns which read an encode does — the plan its count came from, or a reopen for an image past the open-descriptor bound.ServerRequestImages.plannedEntriestakes the dictionary snapshot, the plan pass and the id-to-plan pairing together.[UUID: URL]has no order, so pairing ids and plans from two walks files one image's features under another's id — and when both images project to the same soft-token count, nothing downstream notices.Same order, same open-plan bound, same cancellation points.
Also here
invalid_imageinstead of 500. Admission reads dimensions with stream verification off and the encode re-plans with it on, so a truncated image is admitted and fails the second read. That failure reached the generic handler as a server error, which official clients retry and 4xx they do not.channel.getOptionresolving to theEventLoopFutureoverload rather than theasyncone, andAttachment.record(_ image: NSImage, as: .png), which the Testing library only gained in 6.3. The frame renderer forks on#if compiler(>=6.3)rather than moving wholesale to PNGData, because the image overload serializes lazily — attachingDataunconditionally made every recorded frame pay an encode it discards, one of them inside a live timing gate.build-oldest-supported-toolchainCI job selecting Xcode 26.3, so this cannot regress silently again. Build only: running the suite twice would double CI time.Verification
swift build -c release, Swift 6.2.4swift build, Swift 6.3.3swift test --filter "ServerRequestImagesTests|ServerIngressHardeningTests|Transcript"mainThe A/B used a loaded Gemma 4 with the companion vision pack and solid-colour fixtures, so a wrong answer is unambiguous:
mainRed, prompt 283first: red, second: blue, prompt 560first: blue, second: red, prompt 560Blue, prompt 559internal_errorinvalid_imageThe reversed pair is the end-to-end form of the pairing invariant: a swapped zip would name the colours the wrong way round.
Periphery (
--retain-public, run from this checkout): 874 findings against 872 onmain. The delta is three Swift Testing@Testfunctions and a private test helper, the same macro false-positive class that accounts for all 872. No production symbol added here is flagged.