Skip to content

[Bug]: Frequent cmd.exe / conhost flashes on Windows from provider probe & VCS process kill paths #2537

Description

@samvdst

Before submitting

  • I searched existing issues and did not find a duplicate.
  • I included enough detail to reproduce or investigate the problem.

Area

apps/server

Steps to reproduce

  1. Install T3 Code v0.0.22 on Windows 11.
  2. Open a project with at least one provider (Claude/Cursor/Codex/OpenCode) configured, and a git repo.
  3. Leave the app running and watch the screen.

Expected behavior

No transient cmd.exe / conhost windows appear during normal idle / agent activity.

Actual behavior

Roughly every 5 minutes, a burst of console windows briefly flashes. With Process Monitor I captured 43 short-lived cmd.exe processes in a ~1 second window, all of the form:

C:\WINDOWS\system32\cmd.exe /d /s /c "taskkill /pid <N> /T /F"

In the same window: 114 git.exe → conhost.exe and 9 gh.exe → conhost.exe events from interleaved checkpoint / SCM activity.

Root cause

All flashes route through Effect's NodeChildProcessSpawner.killProcessGroup on Windows, which uses child_process.exec("taskkill /pid <pid> /T /F") — and exec always wraps in %ComSpec% /d /s /c … (no windowsHide). Anything that calls child.kill() on a handle from Effect's spawner produces the flash.

Three v0.0.22 changes added new code paths that hit this on a regular cadence; v0.0.21 didn't:

1. OpenCode probe was rewritten with shell:true + dual SIGTERM/SIGKILL kill (commit 3582288, "Stop OpenCode refresh from leaking serve processes").

apps/server/src/provider/opencodeRuntime.ts:

  • Spawn now sets shell: process.platform === "win32" on opencode serve (was direct in v0.0.21) → cmd.exe wrapper flash on every 5-minute refresh.
  • New terminateChild finalizer unconditionally fires SIGTERM via child.kill({ killSignal, forceKillAfter: "1s" }), sleeps 1s, then SIGKILL — both routed through killProcessGroup on Windows, both flash. Total: 3 cmd.exe events per OpenCode probe, where v0.0.21 had 0.

2. VcsProcess adds an explicit child.kill() finalizer on every git invocation (commit 6d7fe2e, "Introduce pluggable VCS driver foundation").

apps/server/src/vcs/VcsProcess.ts:155:

const child = yield* spawner.spawn(...);
yield* Effect.addFinalizer(() => child.kill().pipe(Effect.ignore));

Effect.addFinalizer(child.kill()) runs on every git scope close, even when git already exited cleanly. child.kill() always tries killProcessGroup first → cmd.exe + taskkill flash per git command. Active consumers via VcsDriverRegistry.makeGitVcsDriver.makeVcsDriverShape():

  • apps/server/src/checkpointing/Layers/CheckpointStore.ts:40-42 — every checkpoint capture (5–10 git commands per agent turn).
  • apps/server/src/sourceControl/SourceControlProviderRegistry.ts:159-171, apps/server/src/sourceControl/BitbucketApi.ts:435, apps/server/src/git/GitWorkflowService.ts (ensureGit, detectGitRepositoryForStatus).
  • apps/server/src/vcs/VcsProvisioningService.ts:46.

The v0.0.21 equivalent (apps/server/src/git/Layers/GitCore.ts) only added an addFinalizer for the trace2 monitor — never an explicit child.kill(). The spawner's internal acquireRelease already no-ops on clean exit, so the new explicit finalizer is redundant on the success path and is what fires the flash on every git command.

3. SSH + Tailscale integrations shell-wrap on Windows (commit 3772fa1, "feat: Hosted Frontend, Tailscale Integration & SSH Lancher").

packages/tailscale/src/tailscale.ts:134,213, packages/ssh/src/command.ts:177, packages/ssh/src/tunnel.ts:982 all spawn with shell: process.platform === "win32" and rely on the same kill path on scope close.

Why the 5-minute burst

makeManagedServerProvider (apps/server/src/provider/makeManagedServerProvider.ts:133-138) has each provider run Effect.forever(sleep(5min) >> refreshSnapshot()).pipe(Effect.forkScoped). All four providers' loops start together at boot and fire roughly synchronously every 5 minutes. ProviderSessionReaper (apps/server/src/provider/Layers/ProviderSessionReaper.ts:12,115) sweeps idle sessions on the same 5-minute cadence, each reaped OpenCode session adding 2 more cmd.exe events. The Multi-Provider per-instance refactor (commit 08e6d4c) means probe count scales with configured instance count.

Suggested fixes (all local; no Effect changes required)

  1. apps/server/src/provider/opencodeRuntime.ts:337 — drop shell: process.platform === "win32" from the opencode serve spawn (v0.0.21 didn't have it).
  2. apps/server/src/provider/opencodeRuntime.ts:356-369 — guard terminateChild so it skips when the child has already exited, or replace killProcessGroup with a plain childProcess.kill(signal) on Windows when the process is still alive.
  3. apps/server/src/vcs/VcsProcess.ts:155 — remove the explicit Effect.addFinalizer(() => child.kill()). The spawner's internal acquireRelease already cleans up non-zero / still-running children.
  4. Broader cleanup: stop passing shell: process.platform === "win32" for binaries that aren't .cmd/.bat shims (most call sites — see processRunner.ts:155, tailscale.ts, ssh/command.ts, ssh/tunnel.ts, the Cursor/Claude/Codex provider probes), and consider upstreaming a windowsHide: true option into @effect/platform-node-shared's NodeChildProcessSpawner.spawn.

Impact

Slows me down / annoying

Version or commit

0.0.22

Environment

T3 Code Desktop v0.0.22 on Windows 11 Enterprise 26200 (10.0.26200).

Logs or stack traces

Process Monitor capture (60s window): 43× cmd.exe /d /s /c "taskkill /pid <N> /T /F", 114× git.exe → conhost.exe, 9× gh.exe → conhost.exe.

Screenshots, recordings, or supporting files

No response

Workaround

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions