Repository navigation
Cold uvx install of the attach bridge flashes a visible terminal window on Windows #851
Description
Activity
dsarno commented
on Aug 25, 2026 ContributorAuthorMore actionsMaintainer direction: keep this open, with a fresh Windows reproduction as the next gate.
Astral merged uv #19235 on 2026-08-11, fixing a related Windows
pythonwlauncher-mode bug. That change does not obviously cover the cold-uvxenvironment-build workers observed here—the probe saw...\\Scripts\\python.exeprocesses—so it is not enough to close this without rerunning the deterministicEnumWindowsprobe against the latest uv.If the flash still reproduces:
- file the minimal probe upstream with the
CASCADIA_HOSTING_WINDOW_CLASSevidence and exact process tree; - link that upstream issue here;
- use pre-warming of the exact pinned
godot-ai==Xenvironment during the user-initiated Configure flow as the local mitigation, with visible install/progress state in the dock; - keep client startup on the already-warm environment.
Do not add a speculative hidden-console inheritance trick without a Windows Terminal acceptance test; ConPTY behavior is the part that makes this failure non-obvious.
- file the minimal probe upstream with the
This has a second symptom, reported independently on Discord, with the same root cause — worth folding in here rather than tracking separately.
The other symptom. Every couple of hours the Godot AI tools stop being visible to the client, and the user has to restart both the client and Godot. Their MCP logs show a clean stdio recycle after ~20.6 h, then a failed reconnect:
Starting connection with timeout of 30000ms Server stderr: Installed 69 packages in 12.71s Connection failed after 30105ms (CONNECT_TIMEOUT)A later reconnect succeeded in 7.2 s once the cache was warm. So: not a crash, not the plugin — a cold env build that didn't fit inside the client's default 30 s connect timeout. Exactly this issue's cold path, surfacing as a timeout instead of a window flash.
Measurements (on
main, macOS, this tree):godot-ai==3.2.2resolves to 67 packages / ~78 MB (the reporter's 69 is the same tree plus Windows-only extras).- Cold
uvx --from godot-ai==3.2.2 godot-ai --version: full env build. Warm: 0.11 s. - The weight is upstream and not trimmable from our side:
fastmcpis exactlyfastmcp-slim[client,server], and we use both. Resolved counts — bare slim 16,[client]51,[server]65,[client,server]65. Dropping the client extra saves nothing.
Why it's become more frequent. Configure writes an exact
godot-ai==Xpin, so every version bump guarantees a never-built environment on the next client launch. That matches the reporter's "it used to not happen but it's semi-regular now".A gap worth noting.
prewarm_server_package(#890) fires only on self-update and server-lifecycle recovery. Neither covers the common case: the plugin adopts an already-running server, so no uvx spawn happens, nothing warms the env, and the user's first client launch pays in full.One thing I checked that could have quietly broken any pre-warm fix: the client entry carries
--link-mode copyand the prewarm argv does not. Empirically this does not split uv's environment cache — a--link-mode copylaunch reuses an env warmed without the flag — so warming without it is correct. Worth remembering as the silent-no-op failure mode if uv's cache keying ever changes.Two claims from that Discord thread that don't hold, in case they resurface:
- "config pins
godot-ai==3.2.2but the server reports 3.4.7" — 3.4.7 isfastmcp's version, not ours. godot-ai's latest is 3.2.2 and 3.4.7 has never existed on PyPI. Not a version mismatch. - Setting
MCP_TIMEOUTin the generated config would not help: clients read it from their own process env, not from the server entry'senvblock. It is not something we can fix from the config we write.
#894 implements steps 3 and 4 of the direction above (pre-warm the pinned env during Configure,
Installing…progress in the dock, client startup stays on the warm env). It deliberately does not touch the hidden-console approach, and the WindowsEnumWindowsre-run named as this issue's gate is still outstanding — so this stays open for the upstream question.🤖 Generated with Claude Code
- added a commit that references this issue
on Aug 28, 2026 Reran the deterministic
EnumWindowsprobe on Windows — the gate named above. It no longer reproduces. Closing.Environment
- Windows 11 Pro 10.0.26200, Windows Terminal as default console host
- uv 0.9.13 (the machine's installed version, predates uv#19235) and uv 0.12.7 (latest, released 2026-08-27)
godot-ai==3.2.4from PyPI, 69 packages- Cold cache forced with an isolated
UV_CACHE_DIRper run, so every run genuinely built an environment uv had never built
Precondition check first
A negative result here is worthless unless Windows Terminal really is the default host, so that was verified independently rather than assumed.
HKCU\Console\%%Startuphas neitherDelegationConsolenorDelegationTerminal(= "Let Windows decide"), and a plainCREATE_NEW_CONSOLEspawn ofcmd.exeproduces:Windows Terminal 67c9717e9ff0cc09 pid=29432 CASCADIA_HOSTING_WINDOW_CLASS pid=29432 title='Windows Terminal' PseudoConsoleWindow pid=33664So the
CASCADIA_HOSTING_WINDOW_CLASSthe original probe keyed on is exactly what this machine produces for a console app. The precondition holds.Results
Each run spawns the verbatim
_WINDOWS_STDIO_BOOTSTRAPshape fromclient_configurator.gd—pythonw.exe -c <bootstrap> uvx --link-mode copy --from godot-ai==3.2.4 …— withCREATE_NO_WINDOW | DETACHED_PROCESS, mimicking a GUI MCP client, while pollingEnumWindowsat 50 ms forCASCADIA_HOSTING_WINDOW_CLASS,CASCADIA_WINDOW_CLASS,ConsoleWindowClassandPseudoConsoleWindow.run uv argv cold build new console-host windows baseline 0.9.13 godot-ai --version69 pkgs, 9.23 s 0 latest 0.12.7 godot-ai --version69 pkgs, 9.72 s 0 gate 0.12.7 godot-ai attach --port 18130 --ws-port 19630 --disable-telemetry69 pkgs, 22.32 s 0 The third run is the faithful one: the exact client argv, and the bridge reached
Starting MCP server 'Godot AI attach' with transport 'stdio'— so the whole cold path ran, build through bridge startup, with nothing appearing.Note the baseline: it does not reproduce on the old uv either. That matters for attribution — this is not evidence that uv#19235 fixed it, and the cause of the original sighting stays unexplained.
One correction to this issue's stated root cause
The issue attributes the windows to "uv's own child
python.exeprocesses during env creation". The cold cache shows that is not what those paths are.archive-v0/<hash>/containspyvenv.cfg,Scripts/activate,Lib/site-packages/{godot_ai,fastmcp,…}— it is the ephemeral tool environment venv itself, which uv stores underarchive-v0. So…\archive-v0\…\Scripts\python.exewas the tool env's own interpreter — the bridge process — not a uv build worker. Worth knowing if this resurfaces: the thing to instrument is the entry-point exec, not uv's installer.Also confirmed: no source builds occur (
builds-v0empty, all 69 deps resolve to wheels), so there is no build-backend worker path to blame.Why closing rather than parking
- The gate this issue was held open for has been run and is negative on the machine where the flash was originally observed, with the precondition verified.
- The second symptom folded in above — the cold env build overrunning an MCP client's default 30 s connect timeout — is addressed by fix: pre-warm the pinned uv environment at Configure time (#851) #894, which pre-warms the pinned environment during the user-initiated Configure flow and closes the adoption gap (plugin adopts a running server → no uvx spawn → nothing warms the env).
- Nothing speculative was added: no hidden-console inheritance trick, per the direction above.
Reopen with a fresh probe log if the flash is seen again — ideally with the uv version and the offending window's class, title and PID, since that identifies which process in the tree owns the console.
Probe script (keeping it here so it outlives the session this time)
"""EnumWindows probe for #851. Usage: python probe.py <uvx.exe> <pythonw.exe> <version> [seconds] Force a cold build with UV_CACHE_DIR pointed at an empty directory.""" import ctypes, ctypes.wintypes as wt, subprocess, sys, time user32 = ctypes.WinDLL("user32", use_last_error=True) Proc = ctypes.WINFUNCTYPE(wt.BOOL, wt.HWND, wt.LPARAM) CONSOLEISH = {"CASCADIA_HOSTING_WINDOW_CLASS", "CASCADIA_WINDOW_CLASS", "ConsoleWindowClass", "PseudoConsoleWindow"} # verbatim from client_configurator.gd _WINDOWS_STDIO_BOOTSTRAP BOOTSTRAP = ("import subprocess,sys; raise SystemExit(subprocess.call(sys.argv[1:], " "stdin=sys.stdin, stdout=sys.stdout, stderr=sys.stderr, creationflags=0x08000000))") def snapshot(): found = {} def cb(hwnd, _l): cls = ctypes.create_unicode_buffer(256) user32.GetClassNameW(hwnd, cls, 256) if cls.value not in CONSOLEISH: return True n = user32.GetWindowTextLengthW(hwnd) title = ctypes.create_unicode_buffer(n + 1) user32.GetWindowTextW(hwnd, title, n + 1) pid = wt.DWORD() user32.GetWindowThreadProcessId(hwnd, ctypes.byref(pid)) found[hwnd] = {"class": cls.value, "title": title.value, "pid": pid.value, "visible": bool(user32.IsWindowVisible(hwnd))} return True user32.EnumWindows(Proc(cb), 0) return found uvx, pythonw, version = sys.argv[1:4] duration = float(sys.argv[4]) if len(sys.argv) > 4 else 240.0 baseline = snapshot() args = [pythonw, "-c", BOOTSTRAP, uvx, "--link-mode", "copy", "--from", f"godot-ai=={version}", "godot-ai", "attach", "--port", "18130", "--ws-port", "19630", "--disable-telemetry"] t0 = time.time() proc = subprocess.Popen(args, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, creationflags=0x08000000 | 0x00000008) # CREATE_NO_WINDOW|DETACHED seen, new = dict(baseline), 0 while time.time() - t0 < duration and proc.poll() is None: for h, w in snapshot().items(): if h not in seen: seen[h] = w; new += 1 print(f"[{time.time()-t0:7.2f}s] APPEAR {w['class']} pid={w['pid']} " f"visible={w['visible']} title={w['title']!r}", flush=True) time.sleep(0.05) print(f"new console-ish windows: {new}")
🤖 Generated with Claude Code
Observed live on Windows 11 with Windows Terminal as the default terminal host, and reproduced deterministically with an EnumWindows probe.
When an AI client spawns the attach bridge for a version uv has never built on that machine (first spawn after a release repin), a visible Windows Terminal window appears for the duration of the env build. Probe evidence:
CASCADIA_HOSTING_WINDOW_CLASSwindows titled…\uv\cache\archive-v0\…\Scripts\python.exeappear ~40s into a colduvx --from godot-ai==X godot-ai attach …spawn and vanish when the build finishes. Warm-cache spawns show nothing.The entry's
pythonw -c <bootstrap>wrapper launchesuvxitself withCREATE_NO_WINDOW, which is why normal operation is silent — but uv's own childpython.exeprocesses during env creation are spawned with uv's flags, not ours, and under Windows-Terminal-as-default they allocate a visible window.Impact: cosmetic, cold-path only — first bridge spawn per (machine × version), typically right after a plugin update repins entries. Still worth fixing: it looks like malware to an unwarned user.
Candidate directions, unverified:
SW_HIDEhandling);CREATE_NO_WINDOWon their workers (upstream fix may exist).Probe script available from the 3.1.0 verification session; reproduce with any never-built version pin under
pythonw.🤖 Generated with Claude Code