Skip to content

DAG runner idle timeout kills tasks during an in-progress SDK reconnect #59

Open

Description

@rajarshidattapy

Description

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:

https://github.com/cursor/cookbook/blob/main/sdk/dag-task-runner/src/run_dag.ts#L363-L375

const timeoutForNext = Math.min(deadline - Date.now(), streamIdleTimeoutMs);
if (timeoutForNext <= 0) {
  throw new TimeoutError(`Task ${task.id} exceeded deadline of ${formatMs(taskTimeoutMs)}`);
}
const next = await withTimeout(
  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.

Related

Activity

  1. deanrie commented on Oct 4, 2026

    @deanrie

    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.

    I'll link back here when the SDK ticket moves.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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