Fix Swift 6.4 region-isolation errors in the server and app decode paths - #3
Open
Osiriss664 wants to merge 1 commit into
Open
Osiriss664 wants to merge 1 commit into
Osiriss664 wants to merge 1 commit into
Conversation
Swift 6.4 treats a local function declared inside an actor method as actor-isolated, so the progress callbacks in ServerInference and RealInferenceClient became 'self'-isolated and could no longer be sent to the concurrent runRawCompletion. Box the mutable per-request state the same way ProgressState already does, turn the event publishers into @sendable closures, and mark both progress callbacks @sendable. No behavior change.
This branch has not been deployed
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.
What changed and why
On macOS 27 with Swift 6.4,
swift build -c releasefails on a clean checkoutof v5.0.1. CI runs on
macos-26(Swift 6.2), where the same code compiles, sothis is not visible upstream yet. Anyone on a current macOS hits it immediately.
Seven errors in two files, all
#RegionIsolation::SendingRisksDataRace:Both call sites build a progress callback inside an actor-isolated method and
hand it to
runRawCompletion, a@concurrentnonisolated global function. Twothings make that callback non-sendable under 6.4:
content,calls,stopMatcher,shouldStop,responseText,assistantDecodeError) that are also read after the callreturns, so region analysis sees a use-after-send.
treats such a local function as actor-isolated, which makes the enclosing
closure
'self'-isolated. The compiler note states this directly.The change
Both files now follow the pattern this repository already uses in
RealInferenceClient.swift(ProgressState): keep the per-request mutablestate in a small
@unchecked Sendablebox, and make the event publisher a@Sendableclosure instead of a local function.Sources/TUFFServer/Core/ServerInference.swiftprivate final class ServerDecodeState: @unchecked SendableholdingstopMatcher,content,calls,decodingError,shouldStop.handle(_:)becomes a@Sendable ([StructuredAssistantEvent]) -> Voidclosure.
runRawCompletionprogress closure is marked@Sendable.Sources/TUFFApp/Core/Inference/RealInferenceClient.swiftresponseTextandassistantDecodeErrorinto the existingProgressStatebox.publishAssistantEventsbecomes a@Sendableclosure; its two call sitesdrop the
index:andelapsed:argument labels, since closures have none.@Sendable.Design and compatibility decisions
ProgressState. The callback runs synchronouslyinside
runRawCompletionon the same task while the actor method awaits it,so the boxed state is only ever touched sequentially. That is the reasoning
already documented on
ProgressState; this PR extends it rather thaninventing a new one.
@Sendableclosures and@unchecked Sendableclasses both exist in 6.2, so this should be neutral for the current CI
toolchain. I could not verify that myself (see Limitations).
are unchanged. No runtime switch was added, no production default changed.
.gturbocompatibility, fail-closed image input, or the loopback-only serveris affected.
-Xswiftc -swift-version -Xswiftc 5breaksswift-jinja, which uses bare slash regex literals that require Swift 6 mode,and then trips over
IsolatedDefaultValuesinAppModel.swift:366. Runningthe Swift 6.2 toolchain via swiftly crashes the frontend against the macOS 27
SDK.
is nothing new to assert. The build itself is the signal; a
macos-27CIrunner would cover this class of breakage (see Follow-ups).
Tests run
Machine: MacBook Air M5, 16 GB unified memory, macOS 27, Swift 6.4, Apple
Silicon.
Ran:
swift build -c release— succeeds, no errors, no new warnings.Scripts/test.sh— 452 tests in 63 suites, 4 failing.ruby Scripts/check_brand_assets.rb— passed.ruby Scripts/check_markdown_links.rb— passed, 10 Markdown files checked.Did not run:
ruby Scripts/check_app_version.rb(needs a token) andScripts/package_app.sh(packaging is untouched by this change).The four failures are outside the changed code paths, and all four look
specific to this machine:
MPPPrefillInt4QMMTests.bf16OutputProjectorPreservesValuesAboveFP16RangesupportsFamily(.apple10); here a different kernel path is selected and the output is clipped to FP16RoPETests"GPT-OSS YaRN matches reference", position 131071ServerIngressHardeningTests.connectionsBeyondTheCapAreClosed.systemCall("connect", 54), instead of closing cleanlyAppModelRestrictionBypassTests.hardwareGateRefusesDownloadAndSelectionUntilBypassedA clean-tree baseline is not possible here: without this change the package does
not compile under Swift 6.4, so there is no comparison run. If these four also
fail on qualified hardware they are unrelated to this PR; if they pass there,
they are M5-specific and worth a separate issue. I am happy to file that
separately with full logs.
Real-model checks
Preflight per CONTRIBUTING: macOS 27, Swift 6.4, free disk available, no other
TUFF app, CLI, server, decode service, model test, or MLX process running. One
model process at a time.
TUFFRepack --model gemma4installed,--verify-installpassed.TUFFRepack --model qwen36installed,--verify-installpassed.TUFFCLIagainst Gemma 4 26B-A4B and Qwen3.635B-A3B: coherent output, streaming intact, stop handling and structured
decoding behaved as before.
TUFFServer --model scratch/gemma4.gturbo --port 8080:/health,/v1/models, and/v1/chat/completionsall answer correctly on127.0.0.1.TUFFCLI(long system prompt plus alarge pasted text body) produced a correct, complete answer.
These were qualitative checks that the decode path still works, not benchmark
measurements. I am deliberately making no performance claim: my numbers come
from an unqualified Mac and would not be a valid speed result.
Limitations and hardware not available
Swift 6.2. CI should show that.
above may well be M5-specific rather than related to this change.
Follow-ups (not in this PR)
macos-27runner to.github/workflows/ci.ymlso thisclass of breakage surfaces before release.
Tools do not ship the
SwiftUIMacrosplugin that@Statenow requires.Engine, CLI, server, and repack build fine with Command Line Tools alone. That
is a README note rather than a code change; I can open it separately if you
want it.
AI assistance
Claude (Opus) was used throughout, and substantially:
the actor-isolation cause, and wrote both diffs. The
ServerDecodeStateboxand the
@Sendableclosure conversions are generated code that I reviewedline by line before applying.
built it and drafted this pull request text.
sections above was run by me on my own machine, and every number and test name
in this PR comes from my own logs. Nothing here is reported second-hand or
from a model's assumption.
Context for the review: I am a security analyst, not a Swift developer. I
understand what this change does and why it is needed, and I can explain the
isolation problem it solves, but I cannot independently judge whether it is the
solution you would prefer for this codebase. If you would rather solve it
differently, for example by marking
runRawCompletionasnonisolated(nonsending)or by restructuring the callbacks, treat this PR as areproducible bug report with a working patch attached, and feel free to
implement it your own way. Raw logs for everything above are available on
request.