Problem
On Windows, the SGLang Qwen3.8-27B stack has exactly the failure the vLLM stack just stopped having.
server/lib/sglangQwenProject.js resolves its default project directory to a Windows home (%USERPROFILE%\sglang-qwen38), and the sglang row in server/services/localRuntimeSetup.js declares platforms: ['linux', 'win32']. But the compose project and its weights live inside a WSL2 distro — Docker Desktop's engine is a WSL2 VM — so a native-Win32 PortOS reads neither the checkout nor its models/, sglangStartBlockedReason refuses, and the operator is handed SGLANG_QWEN_PROJECT_DIR to fill in with a \\wsl.localhost\<distro>\home\<user>\… UNC path they have to look up themselves.
This is more acute for SGLang than it was for vLLM: SGLang ships no provisioner, so preparing it by hand from docs/features/sglang-qwen38.md — inside the distro — is the only path. Every Windows operator therefore hits the refusal.
What already exists
The machinery landed with the vLLM fix and is deliberately generic:
server/lib/wslDistro.js — detectWslProjectDir(leaf) takes the directory name as a parameter and returns {dir, distro, home} or {dir: null, reason}. Nothing in it is vLLM-specific.
server/services/vllmQwenManager.js — ensureVllmProjectDir() (settle → detect → record → emit) and wslPlacementRefusal(found) (a reason → lead sentence table plus one shared tail).
server/lib/vllmQwenProject.js — readRecordedVllmProjectDir / recordVllmProjectDir / vllmProjectDirIsSettled, all keyed on one env-var name and one .env path.
Only three values are stack-specific in that loop: the env-var name, the directory leaf, and the size hint in the prose.
Decision (already made — do not re-litigate)
Lift the loop, keep the prose local.
- Add
ensureWslProjectDir({ envVar, leaf, sizeHint, isSettled, record, emit }) to a shared home — server/services/wslProjectPlacement.js is the natural spot, since it both spawns and writes and so cannot live in a lib/ that promises neither.
- Generalise the
.env record helpers in vllmQwenProject.js to take the key (readRecordedProjectDir(envVar, envPath?) etc.) and move them beside upsertEnvLine, or add an SGLang-keyed pair that calls the same primitives. Do not leave two copies.
- Rewrite
vllmQwenManager.ensureVllmProjectDir as a call into it, keeping wslPlacementRefusal's vLLM wording where it is (refusal prose stays per-stack — SGLang's points at its feature doc, not at a clone).
- Call it from the
sglang row's start in server/services/localRuntimeSetup.js, before inspectSglangQwenProject().
- Update
docs/features/sglang-qwen38.md's Windows section the way docs/features/qwen38-rtx3090.md §1c was updated: detection is automatic, SGLANG_QWEN_PROJECT_DIR overrules it.
SGLang gets detect + record only, not placement — there is no provisioner to clone into the directory, so the refusal cases still point at the doc.
Acceptance
- On Windows with a normal WSL2 distro, the SGLang Start button finds a hand-prepared project with no environment variable set, and records the UNC path so the readiness poll agrees.
wslPlacementRefusal-style refusals still name that host's fix for no-wsl / no-distro / internal-distro / unreadable-share.
- Exactly one implementation of the settle→detect→record loop in the tree.
- An exported
SGLANG_QWEN_PROJECT_DIR still wins over the recorded value.
Context
Follow-up from the vLLM Windows placement change; raised by the altitude pass on that PR.
Problem
On Windows, the SGLang Qwen3.8-27B stack has exactly the failure the vLLM stack just stopped having.
server/lib/sglangQwenProject.jsresolves its default project directory to a Windows home (%USERPROFILE%\sglang-qwen38), and thesglangrow inserver/services/localRuntimeSetup.jsdeclaresplatforms: ['linux', 'win32']. But the compose project and its weights live inside a WSL2 distro — Docker Desktop's engine is a WSL2 VM — so a native-Win32 PortOS reads neither the checkout nor itsmodels/,sglangStartBlockedReasonrefuses, and the operator is handedSGLANG_QWEN_PROJECT_DIRto fill in with a\\wsl.localhost\<distro>\home\<user>\…UNC path they have to look up themselves.This is more acute for SGLang than it was for vLLM: SGLang ships no provisioner, so preparing it by hand from
docs/features/sglang-qwen38.md— inside the distro — is the only path. Every Windows operator therefore hits the refusal.What already exists
The machinery landed with the vLLM fix and is deliberately generic:
server/lib/wslDistro.js—detectWslProjectDir(leaf)takes the directory name as a parameter and returns{dir, distro, home}or{dir: null, reason}. Nothing in it is vLLM-specific.server/services/vllmQwenManager.js—ensureVllmProjectDir()(settle → detect → record → emit) andwslPlacementRefusal(found)(areason → lead sentencetable plus one shared tail).server/lib/vllmQwenProject.js—readRecordedVllmProjectDir/recordVllmProjectDir/vllmProjectDirIsSettled, all keyed on one env-var name and one.envpath.Only three values are stack-specific in that loop: the env-var name, the directory leaf, and the size hint in the prose.
Decision (already made — do not re-litigate)
Lift the loop, keep the prose local.
ensureWslProjectDir({ envVar, leaf, sizeHint, isSettled, record, emit })to a shared home —server/services/wslProjectPlacement.jsis the natural spot, since it both spawns and writes and so cannot live in alib/that promises neither..envrecord helpers invllmQwenProject.jsto take the key (readRecordedProjectDir(envVar, envPath?)etc.) and move them besideupsertEnvLine, or add an SGLang-keyed pair that calls the same primitives. Do not leave two copies.vllmQwenManager.ensureVllmProjectDiras a call into it, keepingwslPlacementRefusal's vLLM wording where it is (refusal prose stays per-stack — SGLang's points at its feature doc, not at a clone).sglangrow'sstartinserver/services/localRuntimeSetup.js, beforeinspectSglangQwenProject().docs/features/sglang-qwen38.md's Windows section the waydocs/features/qwen38-rtx3090.md§1c was updated: detection is automatic,SGLANG_QWEN_PROJECT_DIRoverrules it.SGLang gets detect + record only, not placement — there is no provisioner to clone into the directory, so the refusal cases still point at the doc.
Acceptance
wslPlacementRefusal-style refusals still name that host's fix forno-wsl/no-distro/internal-distro/unreadable-share.SGLANG_QWEN_PROJECT_DIRstill wins over the recorded value.Context
Follow-up from the vLLM Windows placement change; raised by the altitude pass on that PR.