@cursor/sdk 1.0.26 — run ends with bidi_append_deadline_exceeded after ~7.7 min shell tool (seqno=0, 60105ms deadline)

Where does the bug appear (feature/product)?

Cursor SDK

Describe the Bug

We run a long-lived Node.js HTTP service that wraps @cursor/sdk for platform automation (Background Agents API, local runtime). During an agent run that had already streamed 163 events over ~7.7 minutes, run.stream() emitted a terminal status=ERROR whose message is:

[deadline_exceeded] bidi_append_deadline_exceeded: append seqno=0 (6832 bytes) exceeded 60105ms deadline

run.wait() then resolved (did not throw) with the same message under error.message. Our host only forwards these SDK events — we do not synthesize bidi_append_deadline_exceeded anywhere in our code.

Steps to Reproduce

  1. Install @cursor/[email protected] on Node.js 22.x inside a containerized Linux sandbox (K8s pod, workspace on mounted volume).

  2. Create a local agent and start a multi-step agent run (reads + shell commands, resume/continue prompt):

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

await Cursor.me({ apiKey: process.env.CURSOR_API_KEY! });

const agent = await Agent.create({
  apiKey: process.env.CURSOR_API_KEY!,
  model: { id: "auto" },
  local: { cwd: "/workspace/apps/my-repo" },
});

const run = await agent.send("Continue the interrupted session from workspace state…", {
  model: { id: "auto" },
});

for await (const event of run.stream()) {
  if (event.type === "status" && event.status === "ERROR") {
    console.log("terminal status:", JSON.stringify(event));
    break;
  }
}

const result = await run.wait();
console.log("wait:", JSON.stringify(result));
  1. Let the run proceed through several tool calls (including shell) for several minutes.

  2. Observe terminal status=ERROR with bidi_append_deadline_exceeded and run.wait() returning status: "error" with the same message — after substantial streamed output (not an instant startup failure).

We have not isolated a minimal repro yet — reporting with full IDs/logs first to confirm origin and recommended recovery before investing in a standalone repro script.

Expected Behavior

  1. If a BidiAppend times out, the SDK should surface a structured, retryable error (or automatically retry/resume the bidi sequence) rather than failing the entire run after ~7 minutes of successful streaming.

  2. If the root cause is event-stream delivery stall (shell already finished on the client), run.wait() / run state APIs should reflect completed work so callers can resume instead of discarding partial conversation.

  3. Documentation should clarify BidiAppend deadline semantics (seqno, byte size, ~60s limit) for embedded SDK integrations.

Operating System

Linux

Version Information

SDK (not IDE/CLI — we embed @cursor/sdk in a long-running Node service):

@cursor/sdk: 1.0.26
@cursor/sdk-win32-x64: 1.0.26  (local dev; prod sandbox uses linux x64 package)
Node.js: 22.x
Host: Code Worker Agent (HTTP wrapper around Background Agents API, local runtime)
Integration: Agent.create() + agent.send() + run.stream() + run.wait() + run.conversation()

For AI issues: which model did you use?

{ id: "auto" } (server-routed Auto)

Wait result showed "model":{"id":"default"}.

Additional Information

requestId=95ab67a8-bc05-4325-966f-16aa3d0f20d5
run_id=run-3eaa6075-124c-404b-a9dc-c5e576bc6507
agent_id=agent-716de734-7699-4aef-8de4-06fb4006daac

Does this stop you from using Cursor

Sometimes - I can sometimes use Cursor

Hey @JeremyChow, thanks for the detailed report. With the IDs you shared, it was easy to trace.

Quick summary: this message isn’t coming from your code host. It’s coming from the SDK transport layer, like you suspected. It’s a health check on the upload path of the streaming connection. If a single upload call doesn’t finish before its deadline, the run surfaces an error instead of hanging forever. On our side we can see the run successfully reconnected a few times and resumed from checkpoints, but the upload path from your sandbox stayed unresponsive. After the automatic retries, the run eventually ended with this error.

About recovery, your point 2: the conversation state is checkpointed, so calling agent.send() again on the same agent continues from the last checkpoint. So catching this error and re-sending is a supported way to resume without losing already completed work. Completed work doesn’t get dropped.

Root cause: you have long lived idle upload connections going through NAT or proxy egress in a containerized environment. In setups like this, the connection mapping can silently expire while idle, and the next upload hits a dead path. That’s what’s happening here. Things to try:

  • Lower TCP keepalive on the nodes, for example net.ipv4.tcp_keepalive_time=300, so idle mappings don’t expire.
  • If you’re intentionally forcing HTTP/1.1 for the agent transport, the HTTP/2 path is noticeably more stable behind NAT or proxies. It’s worth checking if HTTP/2 is available in your egress.

To narrow it down: are you forcing HTTP/1.1 for the agent transport, or running on Bun instead of Node? That would explain whether HTTP/1.1 was chosen on purpose.

Your points 1 and 3, having a more clearly retryable error instead of a terminal failure, and documenting the BidiAppend deadline semantics seqno, size, about 60s, are both reasonable. I’ve passed them to the team, and I’ll reply here if there’s an update.

Thanks for the clarification — that matches what we were seeing.

On HTTP/1.1 vs Bun:
We are intentionally forcing HTTP/1.1 for the local agent transport via configureCursorSdk({ local: { useHttp1ForAgent: true } }) (env: CWA_USE_HTTP1_FOR_AGENT, default true). We run on Node, not Bun.

Why we switched:
We are in mainland China, but our sandbox egress is already routed through an overseas DNS / network path. We originally moved to HTTP/1.1 because we had seen agent streams hang for a long time with little/no progress during long-lived tool calls (suspected HTTP/2 stall on the long-lived connection). Forcing HTTP/1.1 + SSE reduced those hangs for us at the time — so it was a deliberate mitigation, not an accidental runtime default.

Given your note that HTTP/2 is often more stable behind NAT/proxy:
We’ll try turning HTTP/1.1 off (CWA_USE_HTTP1_FOR_AGENT=false) on a canary sandbox and compare bidi_append_deadline_exceeded / reconnect rates, while also looking at lowering TCP keepalive (net.ipv4.tcp_keepalive_time=300) on the nodes as you suggested.

Two follow-ups if helpful:

  1. For environments that already use overseas egress (but still sit behind container NAT), do you generally recommend preferring HTTP/2 over HTTP/1.1 for agent transport?
  2. When we catch this error and call agent.send() again on the same agent, is there any guidance on backoff / whether we should dispose+recreate the agent after N consecutive bidi_append_deadline_exceeded failures?

Appreciate the root-cause detail — this unblocks how we tune transport on our side.

Glad the root cause detail helped. On your two questions:

  1. Yes, for overseas egress behind container NAT we generally recommend HTTP/2 over HTTP/1.1 for agent transport. With HTTP/2 the whole stream runs over a single multiplexed connection, and it survives idle periods behind NAT or a proxy much better than separate unary upload POSTs on HTTP/1.1, where a dead mapping shows up on the next bidiAppend. The good news for your canary is you are already running HTTP/1.1 directly via the SDK useHttp1ForAgent: true, so the comparison is literally just flipping CWA_USE_HTTP1_FOR_AGENT=false on the canary sandbox, with no runtime change. I would apply net.ipv4.tcp_keepalive_time=300 on the nodes before the comparison. Keepalive helps both transports, so it will remove some noise from the experiment. Also keep in mind the original reason you moved away from HTTP/2, stalls on long streams. If those come back, look at idle or proxy timeouts on your egress and keepalive there as well.

  2. On backoff: calling agent.send() on the same agent is a supported way to resume from the last checkpoint, and in your own session the logs show resume worked multiple times and completed work was not lost. There is no exposed knob in the SDK right now for backoff or append timeout, so keep retry logic on your side: bounded exponential backoff like 1s → 2s → 4s with a cap and jitter, and then retry with another send().

On dispose plus recreate after N consecutive errors, that is a reasonable circuit breaker. If the upload path stays dead, repeated send() calls on the same connection will not revive it, so limiting consecutive bidi_append_deadline_exceeded errors like 3 to 5 and recreating the transport or agent is a good defensive practice. One caveat: resume is confirmed to work via agent.send() on the same agent, so I would test continuity with a freshly created agent instance on the same canary before relying on it in prod.

I passed your points about a clearer retryable error and better docs for BidiAppend semantics to the team. I will reply in the thread if there is an update. Let me know how the HTTP/2 vs HTTP/1.1 canary goes.