From fd7320ec0e9e4ffdb4b8581b2bee545336833bd0 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 10 May 2026 11:09:51 +0000 Subject: [PATCH 1/2] Harden DAG runner task startup Co-authored-by: Jon Kaplan --- .../skills/dag-task-runner/scripts/run_dag.ts | 76 +++++++++++++++---- sdk/dag-task-runner/src/run_dag.ts | 76 +++++++++++++++---- 2 files changed, 126 insertions(+), 26 deletions(-) diff --git a/.cursor/skills/dag-task-runner/scripts/run_dag.ts b/.cursor/skills/dag-task-runner/scripts/run_dag.ts index 3c9fcb9..8bf6ec4 100644 --- a/.cursor/skills/dag-task-runner/scripts/run_dag.ts +++ b/.cursor/skills/dag-task-runner/scripts/run_dag.ts @@ -55,6 +55,7 @@ interface RunnerTaskRun { stream: () => AsyncIterable; wait: () => Promise<{ status: string; + result?: string; durationMs?: number; usage?: { inputTokens?: number; outputTokens?: number }; }>; @@ -63,6 +64,8 @@ interface RunnerTaskRun { durationMs?: number; } +type RunnerAgent = Awaited>; + function parseArgs(argv: string[]): CliArgs { const args: Record = {}; for (let i = 0; i < argv.length; i++) { @@ -157,7 +160,7 @@ async function main(): Promise { if (!args.initOnly && !process.env.CURSOR_API_KEY) { throw new Error( - "CURSOR_API_KEY is not set. Export it or `set -a && source .env && set +a` first.", + "CURSOR_API_KEY is not set. Export it before launching the runner.", ); } @@ -337,12 +340,8 @@ async function runTask( ? `${upstreamContext}\n\n---\n\n${task.subtask_prompt}` : task.subtask_prompt; - const agent = await Agent.create({ - apiKey: process.env.CURSOR_API_KEY!, - model: { id: ts.model }, - local: { cwd }, - }); - + let agent: RunnerAgent | undefined; + let disposeAgentOnCreate = false; let run: RunnerTaskRun | undefined; const buffer = new BoundedTextBuffer(STREAM_CAP); let lastPublishAt = 0; @@ -357,7 +356,27 @@ async function runTask( const deadline = Date.now() + taskTimeoutMs; try { - run = (await agent.send(stitched)) as RunnerTaskRun; + const agentPromise = Agent.create({ + apiKey: process.env.CURSOR_API_KEY!, + model: { id: ts.model }, + local: { cwd }, + }); + void agentPromise.then( + (created) => { + if (disposeAgentOnCreate) void disposeAgent(created); + }, + () => {}, + ); + agent = await withTaskDeadline( + agentPromise, + deadline, + `Task ${task.id} exceeded deadline while creating SDK agent`, + ); + run = (await withTaskDeadline( + agent.send(stitched), + deadline, + `Task ${task.id} exceeded deadline while sending prompt`, + )) as RunnerTaskRun; const iterator = run.stream()[Symbol.asyncIterator](); while (true) { const timeoutForNext = Math.min(deadline - Date.now(), streamIdleTimeoutMs); @@ -395,6 +414,7 @@ async function runTask( let result: | { status: string; + result?: string; durationMs?: number; usage?: { inputTokens?: number; outputTokens?: number }; } @@ -432,7 +452,7 @@ async function runTask( ts.durationMs = result.durationMs ?? ts.finishedAt - (ts.startedAt ?? ts.finishedAt); ts.inputTokens = result.usage?.inputTokens; ts.outputTokens = result.usage?.outputTokens; - const rendered = buffer.render().trim(); + const rendered = buffer.render().trim() || renderCappedText(result.result); if (rendered) ts.resultText = rendered; if (result.status === "finished") { @@ -442,6 +462,9 @@ async function runTask( ts.errorMessage = `Run ${result.status}`; } } catch (err) { + if (!agent) { + disposeAgentOnCreate = true; + } if (run && isTimeoutError(err)) { await bestEffortCancel(run, task.id); } @@ -456,10 +479,10 @@ async function runTask( await bestEffortCancel(run, task.id); } publishIfDue(true); - try { - await (agent as unknown as AsyncDisposable)[Symbol.asyncDispose](); - } catch { - // ignore dispose errors + if (agent) { + await disposeAgent(agent); + } else { + disposeAgentOnCreate = true; } writer.schedule(structuredCloneState(state)); } @@ -533,6 +556,26 @@ async function withTimeout( } } +async function withTaskDeadline( + promise: Promise, + deadline: number, + timeoutMessage: string, +): Promise { + const timeoutMs = deadline - Date.now(); + if (timeoutMs <= 0) { + throw new TimeoutError(timeoutMessage); + } + return withTimeout(promise, timeoutMs, timeoutMessage); +} + +async function disposeAgent(agent: RunnerAgent): Promise { + try { + await (agent as unknown as AsyncDisposable)[Symbol.asyncDispose](); + } catch { + // ignore dispose errors + } +} + async function bestEffortCancel( run: { cancel?: () => Promise | void }, taskId: string, @@ -567,6 +610,13 @@ class BoundedTextBuffer { } } +function renderCappedText(text: string | undefined): string { + if (!text?.trim()) return ""; + const buffer = new BoundedTextBuffer(STREAM_CAP); + buffer.append(text); + return buffer.render().trim(); +} + function skipTask( task: RawTask, stateById: Map, diff --git a/sdk/dag-task-runner/src/run_dag.ts b/sdk/dag-task-runner/src/run_dag.ts index 3c9fcb9..8bf6ec4 100644 --- a/sdk/dag-task-runner/src/run_dag.ts +++ b/sdk/dag-task-runner/src/run_dag.ts @@ -55,6 +55,7 @@ interface RunnerTaskRun { stream: () => AsyncIterable; wait: () => Promise<{ status: string; + result?: string; durationMs?: number; usage?: { inputTokens?: number; outputTokens?: number }; }>; @@ -63,6 +64,8 @@ interface RunnerTaskRun { durationMs?: number; } +type RunnerAgent = Awaited>; + function parseArgs(argv: string[]): CliArgs { const args: Record = {}; for (let i = 0; i < argv.length; i++) { @@ -157,7 +160,7 @@ async function main(): Promise { if (!args.initOnly && !process.env.CURSOR_API_KEY) { throw new Error( - "CURSOR_API_KEY is not set. Export it or `set -a && source .env && set +a` first.", + "CURSOR_API_KEY is not set. Export it before launching the runner.", ); } @@ -337,12 +340,8 @@ async function runTask( ? `${upstreamContext}\n\n---\n\n${task.subtask_prompt}` : task.subtask_prompt; - const agent = await Agent.create({ - apiKey: process.env.CURSOR_API_KEY!, - model: { id: ts.model }, - local: { cwd }, - }); - + let agent: RunnerAgent | undefined; + let disposeAgentOnCreate = false; let run: RunnerTaskRun | undefined; const buffer = new BoundedTextBuffer(STREAM_CAP); let lastPublishAt = 0; @@ -357,7 +356,27 @@ async function runTask( const deadline = Date.now() + taskTimeoutMs; try { - run = (await agent.send(stitched)) as RunnerTaskRun; + const agentPromise = Agent.create({ + apiKey: process.env.CURSOR_API_KEY!, + model: { id: ts.model }, + local: { cwd }, + }); + void agentPromise.then( + (created) => { + if (disposeAgentOnCreate) void disposeAgent(created); + }, + () => {}, + ); + agent = await withTaskDeadline( + agentPromise, + deadline, + `Task ${task.id} exceeded deadline while creating SDK agent`, + ); + run = (await withTaskDeadline( + agent.send(stitched), + deadline, + `Task ${task.id} exceeded deadline while sending prompt`, + )) as RunnerTaskRun; const iterator = run.stream()[Symbol.asyncIterator](); while (true) { const timeoutForNext = Math.min(deadline - Date.now(), streamIdleTimeoutMs); @@ -395,6 +414,7 @@ async function runTask( let result: | { status: string; + result?: string; durationMs?: number; usage?: { inputTokens?: number; outputTokens?: number }; } @@ -432,7 +452,7 @@ async function runTask( ts.durationMs = result.durationMs ?? ts.finishedAt - (ts.startedAt ?? ts.finishedAt); ts.inputTokens = result.usage?.inputTokens; ts.outputTokens = result.usage?.outputTokens; - const rendered = buffer.render().trim(); + const rendered = buffer.render().trim() || renderCappedText(result.result); if (rendered) ts.resultText = rendered; if (result.status === "finished") { @@ -442,6 +462,9 @@ async function runTask( ts.errorMessage = `Run ${result.status}`; } } catch (err) { + if (!agent) { + disposeAgentOnCreate = true; + } if (run && isTimeoutError(err)) { await bestEffortCancel(run, task.id); } @@ -456,10 +479,10 @@ async function runTask( await bestEffortCancel(run, task.id); } publishIfDue(true); - try { - await (agent as unknown as AsyncDisposable)[Symbol.asyncDispose](); - } catch { - // ignore dispose errors + if (agent) { + await disposeAgent(agent); + } else { + disposeAgentOnCreate = true; } writer.schedule(structuredCloneState(state)); } @@ -533,6 +556,26 @@ async function withTimeout( } } +async function withTaskDeadline( + promise: Promise, + deadline: number, + timeoutMessage: string, +): Promise { + const timeoutMs = deadline - Date.now(); + if (timeoutMs <= 0) { + throw new TimeoutError(timeoutMessage); + } + return withTimeout(promise, timeoutMs, timeoutMessage); +} + +async function disposeAgent(agent: RunnerAgent): Promise { + try { + await (agent as unknown as AsyncDisposable)[Symbol.asyncDispose](); + } catch { + // ignore dispose errors + } +} + async function bestEffortCancel( run: { cancel?: () => Promise | void }, taskId: string, @@ -567,6 +610,13 @@ class BoundedTextBuffer { } } +function renderCappedText(text: string | undefined): string { + if (!text?.trim()) return ""; + const buffer = new BoundedTextBuffer(STREAM_CAP); + buffer.append(text); + return buffer.render().trim(); +} + function skipTask( task: RawTask, stateById: Map, From 7284fc4f911e5e6c0e5fc6d17c000a3c32db1e8a Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 10 May 2026 11:09:59 +0000 Subject: [PATCH 2/2] Remove unsafe env sourcing from DAG skill Co-authored-by: Jon Kaplan --- .cursor/skills/dag-task-runner/SKILL.md | 10 ++-------- sdk/dag-task-runner/skill/SKILL.md | 10 ++-------- 2 files changed, 4 insertions(+), 16 deletions(-) diff --git a/.cursor/skills/dag-task-runner/SKILL.md b/.cursor/skills/dag-task-runner/SKILL.md index d0f295d..2287b47 100644 --- a/.cursor/skills/dag-task-runner/SKILL.md +++ b/.cursor/skills/dag-task-runner/SKILL.md @@ -137,7 +137,7 @@ Always use the link text `Open Canvas`. Use the absolute path in both the `file: Ensure `CURSOR_API_KEY` is set (the runner fails fast if missing), then launch: ```bash -[ -n "$CURSOR_API_KEY" ] || { [ -f .env ] && set -a && source .env && set +a; } +: "${CURSOR_API_KEY:?Set CURSOR_API_KEY in the environment before running the DAG runner.}" "$RUNNER_DIR/node_modules/.bin/tsx" "$RUNNER_DIR/run_dag.ts" \ --dag /tmp/dag-.json \ @@ -179,18 +179,12 @@ Override any subset inline with top-level DAG `models`, or pass a reusable profi ## Auth -The runner reads `CURSOR_API_KEY` from the environment. Set it however you usually manage secrets: +The runner reads `CURSOR_API_KEY` from the environment. Set it with your usual secrets manager or export it explicitly: ```bash export CURSOR_API_KEY=crsr_... ``` -If the current workspace has a `.env` containing it, source that first: - -```bash -set -a && source .env && set +a -``` - ## CLI options | Flag | Default | Notes | diff --git a/sdk/dag-task-runner/skill/SKILL.md b/sdk/dag-task-runner/skill/SKILL.md index d0f295d..2287b47 100644 --- a/sdk/dag-task-runner/skill/SKILL.md +++ b/sdk/dag-task-runner/skill/SKILL.md @@ -137,7 +137,7 @@ Always use the link text `Open Canvas`. Use the absolute path in both the `file: Ensure `CURSOR_API_KEY` is set (the runner fails fast if missing), then launch: ```bash -[ -n "$CURSOR_API_KEY" ] || { [ -f .env ] && set -a && source .env && set +a; } +: "${CURSOR_API_KEY:?Set CURSOR_API_KEY in the environment before running the DAG runner.}" "$RUNNER_DIR/node_modules/.bin/tsx" "$RUNNER_DIR/run_dag.ts" \ --dag /tmp/dag-.json \ @@ -179,18 +179,12 @@ Override any subset inline with top-level DAG `models`, or pass a reusable profi ## Auth -The runner reads `CURSOR_API_KEY` from the environment. Set it however you usually manage secrets: +The runner reads `CURSOR_API_KEY` from the environment. Set it with your usual secrets manager or export it explicitly: ```bash export CURSOR_API_KEY=crsr_... ``` -If the current workspace has a `.env` containing it, source that first: - -```bash -set -a && source .env && set +a -``` - ## CLI options | Flag | Default | Notes |