Skip to content

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

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

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