Skip to content

Build on the Swift 6.2 toolchains the README promises - #162

Merged
drumih merged 1 commit into
mainfrom
fix/swift-62-build
Aug 28, 2026
Merged

Build on the Swift 6.2 toolchains the README promises#162
drumih merged 1 commit into
mainfrom
fix/swift-62-build

Conversation

@drumih

@drumih drumih commented Aug 28, 2026

Copy link
Copy Markdown
Owner

Fixes #161.

What was broken

swift build -c release failed on every Swift 6.2 toolchain — Xcode 26.0, 26.1, 26.2, 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 "Xcode 26 and Swift 6.2 or newer".

CI never caught it: runs-on: macos-26 resolves to the newest Xcode on the image, currently 26.6 / Swift 6.3.3.

Toolchain Xcode Before After
Swift 6.2 – 6.2.4 26.0 – 26.3 fails, 3 errors builds
Swift 6.3 – 6.3.3 26.4 – 26.6 builds builds

Cause

ServerModelSession is an actor, so visionRuntime and model are self-isolated. 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 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 isolated parameter, a Sendable constraint, 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.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 images project to the same soft-token count, nothing downstream notices.

Same order, same open-plan bound, same cancellation points.

Also here

  • A truncated upload 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.
  • 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 rather than the async one, and Attachment.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 PNG Data, because the image overload serializes lazily — attaching Data unconditionally made every recorded frame pay an encode it discards, one of them inside a live timing gate.
  • 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.
  • Version moves to 0.7.1.

Verification

Gate Result
swift build -c release, Swift 6.2.4 Build complete (121.54s)
swift build, Swift 6.3.3 clean
swift test --filter "ServerRequestImagesTests|ServerIngressHardeningTests|Transcript" 128 tests in 20 suites, green
Both rewritten test cases mutation-checked — reverted the fix, watched them fail
Five real multimodal requests, A/B against main four byte-identical in reply and token counts; the fifth is the intended 500 → 400

The A/B used a loaded Gemma 4 with the companion vision pack and solid-colour fixtures, so a wrong answer is unambiguous:

Request main this branch
One image Red, prompt 283 identical
Two images in one turn first: red, second: blue, prompt 560 identical
Same two, reversed first: blue, second: red, prompt 560 identical
Image in a follow-up turn Blue, prompt 559 identical
Truncated PNG HTTP 500 internal_error HTTP 400 invalid_image

The 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 on main. The delta is three Swift Testing @Test functions and a private test helper, the same macro false-positive class that accounts for all 872. No production symbol added here is flagged.

`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
drumih force-pushed the fix/swift-62-build branch from 75460fb to 3230e08 Compare August 28, 2026 10:11
@drumih
drumih merged commit d50bf4f into main Aug 28, 2026
2 checks passed
@drumih
drumih deleted the fix/swift-62-build branch August 28, 2026 10:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Build fails on Swift 6.2.4 / Xcode 26.3 with visionRuntime SendingRisksDataRace errors

1 participant