@cursor/sdk (local runtime) on Windows: background shells are only partially killed at the end of a run, so grandchild processes are orphaned and keep running

Where does the bug appear (feature/product)?

Cursor SDK

Describe the Bug

nvironment

@cursor/sdk 1.0.31, local runtime (Agent with local: {…}), embedded in an Electron 44 app (main process)
Windows , x64
The same code on macOS/Linux behaves correctly
Summary At the end of each run, the SDK disposes the run’s session runtime, and that aborts all background shells. On POSIX the abort kills the whole process group (process.kill(-pid, …)). On Windows only the top-level shell process is terminated (ChildProcess.kill(), which is TerminateProcess). Nothing kills the process tree, so any process started by the shell survives as an orphan. It keeps running and can keep modifying the workspace after the turn has finished and the UI shows the agent as idle. Pressing Stop (run.cancel()) doesn’t help either: the abort path is the same.

Steps to reproduce

Create a local agent with cwd set to any folder, and send a prompt asking it to start this command in the background: node -e “setInterval(()=>require(‘fs’).appendFileSync(‘stop-test.txt’, Date.now()+‘\n’), 500)”
Let the run finish on its own. Don’t cancel it.
Watch stop-test.txt, and run: Get-CimInstance Win32_Process -Filter “Name=‘node.exe’” | ? CommandLine -like ‘stop-test’ | select ProcessId, ParentProcessId, CreationDate

Steps to Reproduce

Create a local agent with cwd set to any folder, and send a prompt asking it to start this command in the background: node -e “setInterval(()=>require(‘fs’).appendFileSync(‘stop-test.txt’, Date.now()+‘\n’), 500)”
Let the run finish on its own. Don’t cancel it.
Watch stop-test.txt, and run: Get-CimInstance Win32_Process -Filter “Name=‘node.exe’” | ? CommandLine -like ‘stop-test’ | select ProcessId, ParentProcessId, CreationDate

Expected Behavior

When the run ends, the background shell and all its descendants are terminated, which is the current macOS/Linux behaviour. The file stops growing.

The terminal record is marked aborted when the run ends, but node.exe keeps running and stop-test.txt keeps growing until the process is killed by hand. The ParentProcessId of node.exe is the dead shell’s pid from the terminal record.

Terminal record ~/.cursor/projects//terminals/451227.txt:


pid: 20296
cwd: “E:\code\WorkBench”
command: “node -e "setInterval(…)"”
title: “Start interval Node process writing stop-test.txt”
status: aborted
started_at: 2026-10-31T09:19:10.291Z
running_for_ms: 14114


exit_code: unknown
elapsed_ms: 14114
ended_at: 2026-10-31T09:19:24.405Z

The run ended at 09:19:24Z. node.exe was still writing several minutes later.

Operating System

Windows 10/11

Version Information

CURSOR SDK 1.0.31

Additional Information

Code paths (from dist/bundled/index.js in 1.0.31)

When a run’s done settles, it calls await s.dispose() on the per-run session runtime. That reaches backgroundShellExecutor.dispose() → backgroundShellManager.dispose(), which aborts every running background shell.
The shell spawn helper (AJ(…), spawned with detached: true) handles abort like this: if (X.detached) try { process.kill(-Q.pid, “SIGTERM”) } catch { Q.kill(“SIGTERM”) }, followed by SIGKILL after 1 s. On Windows, a negative pid isn’t a process group, so this falls back to killing the single process.
The foreground executor’s abort and timeout handlers check process.platform !== “win32” explicitly and use E.kill(…) on Windows. That also kills a single process only.
The bundle has no taskkill /T call and no Job Object usage.
Suggested fix On Windows, terminate the whole tree: either taskkill /PID /T /F, or spawn shells into a Job Object with JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE. The Codex CLI does the latter. Apply it to both the abort/timeout path and the background-shell dispose path.

Impact The agent appears stopped, but commands it started (code generators, formatters, watchers, scripts) keep running and editing files. Users see changes appear after the turn has ended or after they pressed Stop.

Does this stop you from using Cursor

No - Cursor works, but with this issue

Hi @Arron_Zhou!

This should have been fixed in v1.0.32. The latest version is v1.2.0. Could you try upgrading and see if the issue persists?