@cursor/sdk 1.0.35 and 1.0.36: a backgrounded shell call keeps the Node process alive for 24 hours

Where does the bug appear (feature/product)?

Cursor SDK

Describe the Bug

Gathered 2026-10-06 on linux x64, Node 24.19.0, @cursor/sdk 1.0.36 (the latest on npm that day), local runtime, model gpt-5.4-nano.

Summary

When a local agent’s shell call moves to the background, the SDK arms a ref’d timer equal to the call’s hardTimeout (86,400,000 ms, 24 hours, in our runs) and never clears it: not when the backgrounded command ends, not when the run finishes, not on agent.close(). A Node program that used the SDK therefore cannot exit on its own after such a run; it stays alive until the timer fires a day later.

A call moves to the background in two ways, and both reproduce it:

  1. run.steer(text) while a foreground shell command is running (the SDK moves the command to the background).
  2. The shell call’s own timeout elapsing (the model passed timeout: 3000 for a sleep 20).

Minimal reproduction (SDK only)

mkdir repro && cd repro
npm init -y && npm pkg set type=module
npm i @cursor/[email protected]
# credential: CURSOR_API_KEY, or a prior Cursor.auth.login()

repro.mjs:

import { Agent } from "@cursor/sdk";

const viaSteer = process.argv[2] === "steer";
const agent = await Agent.create({
  model: { id: "gpt-5.4-nano" },
  local: { cwd: process.cwd(), sandboxOptions: { enabled: false } },
});
const prompt = viaSteer
  ? "Use your shell tool to run exactly: sleep 8 && echo SLEPT . Then reply with its output."
  : "Use your shell tool to run exactly: sleep 20 && echo DONE . Give the shell call a timeout of 3000 milliseconds so it moves to the background, then reply with the single word STARTED without waiting for it.";
const run = await agent.send(prompt, { onDelta: () => {} });
if (viaSteer) {
  await new Promise((resolve) => setTimeout(resolve, 4000));
  console.log("steer:", await run.steer("Also include the word PELICAN."));
}
console.log("run:", (await run.wait()).status);
agent.close();
console.log("agent closed: the process should exit now");

Run node repro.mjs (tool timeout) or node repro.mjs steer.

Case Result
node repro.mjs prints run: finished, agent closed, then never exits (killed after 60 s)
node repro.mjs steer steer: complete_delivered, run: finished, agent closed, then never exits (killed after 60 s)
Control: same program, prompt echo HI (nothing backgrounded) exits about 8 s after start

After agent.close(), process.getActiveResourcesInfo() keeps listing a Timeout. A preload that records where each timer is created shows one live timer of 86,400,000 ms, created in the shell executor (Gb.execute, dist/esm/689.js). Clearing that single timer 25 s after agent.close() (diagnostic only) lets the process exit at once, so it is the only thing holding the process.

Where it happens (bundled dist/esm/689.js, offsets from 1.0.35)

In the shell executor’s execute (around offset 689,362):

const T = t.hardTimeout;
void 0 !== T && T > 0 && (I = setTimeout(() => { /* abort the background shell or the command */ }, T));
...
finally { clearTimeout(b), S?.backgroundRequested || clearTimeout(I), R() }

When the command was backgrounded (S.backgroundRequested), I is deliberately left running so the hard timeout can still stop the background shell. The shell is then handed to backgroundShellManager.adopt({...}) without I or a way to clear it, and none of the later paths we found (background completion, the manager’s cleanup, agent.close() releasing the executor lease) clear it. The timer is not unref()'d.

Expected

After a backgrounded command ends, or at the latest when the agent is closed, the hard-timeout timer is cleared (or the timer is unref()'d), so a program that is done exits normally.

Impact and workaround

It does not stop us from using Cursor, since a workaround exists. Any short-lived program that drives the SDK (a CLI, a script, a test) hangs after a turn in which a shell call was backgrounded. Workaround on the caller’s side: exit explicitly once done. OAR does this in oar run (botiverse/oar#122, merged; ships with 0.20.2) and documents it in docs/runtimes/cursor.md.

Steps to Reproduce

see above

Operating System

Linux

Version Information

@cursor/sdk 1.0.36

Does this stop you from using Cursor

No - Cursor works, but with this issue

Hey @xxchan, thanks for the detailed report and repro! I reproduced this on our side. It’s not intended behavior, and we’ve let the team know.

For now, calling process.exit() after agent.close(), as you’re doing in oar run, is the right workaround. I’ll post here when there’s an update.

Hey @xxchan,

This is fixed in @cursor/sdk 1.0.38, which is out on npm now. The 24-hour hard-timeout timer is now cleared once a backgrounded shell command finishes, or when you call agent.close(), so the process can exit on its own.

After updating, the process.exit() call after agent.close() shouldn’t be needed anymore. Let me know if you still see the process hang on 1.0.38 or newer.