Repository navigation
fix(windows): stop console windows from processes this app starts - #180
Conversation
The Windows console hook resolved a bare program name against the server's own PATH. Windows searches the child's PATH. When a caller adds a directory only the child sees, the hook could not find the program, left it alone, and the program opened a visible console window. The device host does this for adb, adding the Android SDK to the child's PATH. The hook now resolves with the PATH and PATHEXT of the child's environment.
On Windows, Effect's spawner ran `taskkill /pid <pid> /T /F` twice for every child that exited non-zero: once from the exit listener and once from the scope release. The process was already gone, so each run failed with "process not found", and each started its own console host. The pid could also belong to an unrelated process by then. Patch @effect/platform-node-shared 4.0.0-rc.115 with the guard Effect 4.0.4 ships: no taskkill once the leader has exited. Drop the patch when the fork moves to Effect 4.0.4 or later.
…e windows The fixture's dev server starts detached, so it has no console. On Windows the watcher it starts then opened a visible console window, several times per run of sessionProcessJob.test.ts. Start every fixture process hidden.
Thread transfer impact✅ Thread transfer remains within every enforced ceiling.
Baseline: Scenario and decoded snapshot size10 historical turns, 5 command tools per turn, 878.9 KiB retained MCP result per historical turn, and a 1.05 MiB retained result in the measured turn.
Updated in place by a trusted workflow. PR artifacts are strictly validated and never executed. |
gh 2.92 can start a detached `gh send-telemetry` child. On Windows that child has no console, so the `tzutil /g` it runs opened a visible console window. The pull request status checks hit this a few times an hour. VcsProcess, which every gh call goes through, now sets GH_TELEMETRY=0 for gh. `gh help environment` documents 0 as off. The user's own gh configuration is not touched.
Review - spec and standardsOne independent read-only review of 42d3152, both axes. Verdict: no blocking finding. The reviewer ran the telemetry test against the old PR 180 review: no blocking findings. |
Problem
On Windows, a black console window flashes while an agent works and when a message is sent. On this PC new consoles open in Windows Terminal, so each flash is a Terminal window.
What makes the windows
I recorded every visible console window for about an hour while sessions worked, and tied each one to the process that opened it. The last round used the event-hook watcher from First Mate #596, which catches a window that lives for 15 ms. Sources:
ghsometimes starts a detachedgh send-telemetrychild, which has no console. Thetzutil /git runs then opens a new visible one. 10 were underfm-daemon, 3 under this app's own pull request statusghcalls, and 3 under thegh-axiSessionStart hook of a new Claude session (the flash after a sent message).ghcalls: fixed here.fm-daemon: First Mate #596. The hook: the user's hook setup.ffmpeg-win64.exe, from agents' browser tests (playwright-cli video-start). Open 6 to 12 seconds each.node.exefromapps/server/src/provider/testFixtures/sessionProcessTree.mjs, 6 per run ofsessionProcessJob.test.ts.powershell Start-Process cmdfrom another session.The processes this app starts itself (
git,gh,taskkill,claude) are hidden by the existing hook inpackages/shared/src/windowsConsole.ts. The Claude Agent SDK also startsclaudewithwindowsHide: true. Claude's children (hooks, MCP servers such asrea.mjs mcpandcmd.exe /c npx) get their own hidden consoles and drew nothing.Playwright and ffmpeg
I ran each case from my copy with a made-up page, under the watcher. I ran each once from a hidden console (as an agent's shell tool runs it) and once from a parent with no console (as a detached daemon runs it):
ffmpegstarted by a script with Node's defaultsffmpeg-win64.exe < node.exe)chromium.launch)chrome-headless-shell.exe < node.exe)recordVideochrome-headless-shell.exeandffmpeg-win64.exe, both< node.exe)So the window comes from a parent with no console, not from
ffmpegor the browser as such. Playwright CLI (@playwright/cli0.1.19, playwright-core 1.63 alpha) starts its session daemon withdetached: true(lib/tools/cli-client/session.jsandprogram.js). On Windows that leaves the daemon with no console. Every browser andffmpegit starts then gets a visible one. This repository starts neither: its preview recording uses the in-window screencast, not a child process.What this fixes in this repository
ghthe server starts runs withGH_TELEMETRY=0. All of them go throughVcsProcess.run, so the variable is set there, forghonly.gh help environmentdocumentsfalseor0as off. The user's ownghconfiguration is not touched.PATH. It resolved a bare name against the server'sPATH, but Windows searches the child's. A program it could not find was left alone and drew a window. The device host does this foradb:LocalDeviceHost.tsadds the Android SDK folders to the child'sPATHand runsadbby name. The hook now readsPATHandPATHEXTfrom the child's environment.taskkillran twice on every child that exited non-zero. Effect's spawner rantaskkill /pid <pid> /T /Ffrom the exit listener and again from the scope release. The process was already gone, so both failed with "process not found". Each run also started its own console host, and by then the pid could belong to an unrelated process. Effect 4.0.4 fixed this by skippingtaskkillonce the leader has exited. This adds a pnpm patch that backports that guard to@effect/platform-node-shared4.0.0-rc.115.sessionProcessJobtest fixture opened console windows. Its dev server starts detached, so the watcher child opened a visible console. The fixture now starts its processes hidden.Measurements
Event-hook watcher, every case started from a parent with no console, as the desktop backend runs.
maingh api rate_limitcalls through the realVcsProcessPATHprobe: a console program on the child'sPATHonly, 2 runstaskkillruns under my server during two Haiku turnsghtelemetry under the Claude hook)sessionProcessJob.test.tsrunDuring the "after"
ghrun the watcher saw one telemetry window. Its parent was the livefm-daemon(gh api ... pulls/594), which does not have First Mate #596 yet. Server turns used an empty temp home and two cheap prompts: "Reply with just the word ok." and one that runsgit status.Tests
apps/server/src/vcs/VcsProcess.test.ts: everyghgetsGH_TELEMETRY: "0", a caller's own variables are kept, andgitis untouched. Before the change it failed withexpected [ undefined, { GH_DEBUG: '' }, undefined ].packages/shared/src/windowsConsole.test.ts: a console child found only on thePATHits caller passed is hidden. On the old hook it fails withexpected false to be true.apps/server/src/process/taskkillAfterExit.test.ts, on Windows only, like the real-process cases insessionProcessJob.test.ts. A child that exits 3 starts notaskkill, and a running child stopped by closing its scope starts exactly one. Without the patch both fail with[ 'taskkill', 'taskkill' ].sessionProcessJob.test.tsstill passes (10 of 10).Fork notes
windowsConsole.tsand the fixture are covered by existingfork-features.jsonentries (ours whole). New entries:gh-telemetry-off(VcsProcess.ts, with its named test) andeffect-taskkill-after-exit(the patch and itspnpm-workspace.yamlline).mainis on Effect 4.0.4, which carries thetaskkillguard. Upstream has noGH_TELEMETRYchange. Upstream v0.0.45 is still on rc.115, so thesync/upstream-v0.0.45merge (chore: merge upstream v0.0.45 #179) needs this patch carried as is.pnpm-lock.yamlmay conflict on the patch hash lines, and regenerating the lock settles it. Drop the patch when the fork moves to Effect 4.0.4.Checks run on Windows
The 11 failures are
scripts/build-desktop-artifact.test.ts, which fails the same onmainon this machine (Tests 11 failed | 60 passed (71)).Test suites of the touched packages:
All 16
apps/serverfailures fail the same onmainon this machine, run as the same files:Tests 16 failed | 297 passed | 12 skipped (325). They are inOpenCodeProvider,userInputAttachments,AgentSessionScanner,GitVcsDriver,GitVcsDriverCore,update,bootService,LocalDeviceHost(a path separator assertion),McpHttpServerandserviceLauncher. Thepackages/sharedfailure isnodeRuntime.test.ts > rejects a hard-linked node alias, which is flaky here and failed 7 of 8 runs onmain.