You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
DAG runner idle timeout kills tasks during an in-progress SDK reconnect #59
sdk/dag-task-runner (and the copied skill at .cursor/skills/dag-task-runner) wraps run.stream() with withTimeout(..., streamIdleTimeoutMs). The default idle window is 5 minutes. If no stream event arrives in that window, the task is marked ERROR as a stalled model.
Headless @cursor/sdk local runs keep enableAgentRetries: true by default. When the transport stalls, the SDK retries internally and emits nothing on the public stream. The runner cannot tell model silence from that reconnect, so it cancels a live retry and the canvas shows a dead task.
The same guard exists in both copies of run_dag.ts:
consttimeoutForNext=Math.min(deadline-Date.now(),streamIdleTimeoutMs);if(timeoutForNext<=0){thrownewTimeoutError(`Task ${task.id} exceeded deadline of ${formatMs(taskTimeoutMs)}`);}constnext=awaitwithTimeout(iterator.next(),timeoutForNext,streamWaitTimeoutMessage({taskId: task.id,timeoutMs: timeoutForNext,
streamIdleTimeoutMs,}),);
Expected
While the SDK is reconnecting, the idle clock should pause. Idle should fire only on real model silence.
Suggested fix
Subscribe to a reconnect/retry signal (onConnectionStateChange / RETRYING) and skip the idle timer while reconnecting. Until @cursor/sdk forwards that hook, wire the runner to the helper in sdk/reconnect-signal (or an equivalent) so transport retries are not logged as task failures.
Hey, the diagnosis is right: run_dag.ts wraps iterator.next() in withTimeout(streamIdleTimeoutMs) and the SDK goes silent on the public stream while it retries, so a reconnect longer than 5 minutes reads as a dead model.
The catch is on the SDK side. I checked @cursor/sdk 1.0.35: the connection-state hook exists internally but nothing exports it, so there is no clean signal for the runner to subscribe to (that's #57, and I've written it up for the SDK team). #62 gets around it by intercepting the SDK's retry log lines, which does pause the idle clock today. If it lands, I'd label the log scraping as a stopgap in the README and the skill, because it stops working the day the log text changes. Once the SDK exports the hook, swapping isRetrying to the real signal is a small change.
Activity
deanrie commentedon Oct 4, 2026
Hey, the diagnosis is right:
run_dag.tswrapsiterator.next()inwithTimeout(streamIdleTimeoutMs)and the SDK goes silent on the public stream while it retries, so a reconnect longer than 5 minutes reads as a dead model.The catch is on the SDK side. I checked
@cursor/sdk1.0.35: the connection-state hook exists internally but nothing exports it, so there is no clean signal for the runner to subscribe to (that's #57, and I've written it up for the SDK team). #62 gets around it by intercepting the SDK's retry log lines, which does pause the idle clock today. If it lands, I'd label the log scraping as a stopgap in the README and the skill, because it stops working the day the log text changes. Once the SDK exports the hook, swappingisRetryingto the real signal is a small change.I'll link back here when the SDK ticket moves.