Where does the bug appear (feature/product)?
Cursor SDK
Describe the bug
A long-lived Node service using @cursor/sdk local agents experienced windows (hours long) where every agent.send() → run.wait() failed in under 3 seconds with {status: "error"}, zero tokens consumed, zero onDelta events, and result: undefined — on resumed sessions and brand-new Agent.create() sessions alike. The real error is never surfaced to the caller: RunResult has no error field. It is only written to the local run store (runs.ndjson, error field), where all 58 failed runs carry the same message:
“Authentication error. If you are logged in, try logging out and back in.”
We initially suspected the cached session token from POST /auth/exchange_user_api_key (a JWT with exp − iat = 3600s, cached in module scope by the SDK) expiring without refresh. Extensive follow-up testing ruled out every client-side trigger we could construct, all against the live API with the same key:
- Probes at t+63/67/72/75/90 minutes past the token TTL all passed — in minimal config and in our full production config (large git workspace,
settingSources: ["project"]with ~330 rules,thinking: mediumparams, per-send HTTP MCP servers), both idle and with periodic activity. - A process SIGSTOP-frozen for 30 minutes and for 8 hours (timers frozen, upstream connections torn down server-side, wall-clock jump on resume) recovered fine on wake.
- Machine sleep was ruled out via the power log (no system sleep occurred on the failure days).
- Injecting 401 responses into
exchange_user_api_keyreproduces the failure signature during the injected outage, but the SDK recovers immediately once the endpoint returns healthy responses — even when the first-ever exchange of the process fails. No persistent client-side poisoning was reproducible.
Conclusion: during the failure windows, the API was actually rejecting this key’s runs. Our dashboard shows we are under quota, status.cursor.com shows no incident for those dates, and since we run with privacy mode our requests likely aren’t retained server-side for lookup. So our questions are: (1) under what conditions does the run path return this in-band “Authentication error…” rejection (key state? per-key throttling? auth-service transients?), and (2) what should we capture client-side on the next occurrence to make this diagnosable — is there a debug flag or verbose logging mode for the SDK’s transport/auth layer, and is the raw HTTP status/error body of the run rejection accessible anywhere client-side?
Independent of the rejections’ cause, three SDK defects verified during the investigation:
run.wait()swallows the error.RunResultcarries no error code/message; the detail exists only in the local run store. Confirmed against the type definitions (run.d.ts) and observed behavior.- In-band auth errors bypass recovery. The re-auth interceptor (
"Auth expired, re-authenticating") and the client-rebuild wrapper only trigger on aConnectErrorwithCode.Unauthenticated. Auth failures delivered as in-band stream error events (ours had no[code]prefix, indicating the status-message path) trigger neither. - A failed key exchange during
Agent.createcrashes bare processes via an unhandled promise rejection.
Steps to reproduce
The rejection windows themselves are not client-reproducible (see the elimination list above). The error-swallowing defect (#1) reproduces deterministically with fault injection:
// Patch fetch BEFORE importing the SDK so /auth/exchange_user_api_key returns 401:
const realFetch = globalThis.fetch;
globalThis.fetch = async (input, init) => {
const url = String(typeof input === "string" ? input : input?.url);
if (url.includes("exchange_user_api_key")) {
return new Response(
JSON.stringify({ error: { message: "Authentication error If you are logged in, try logging out and back in." } }),
{ status: 401, headers: { "Content-Type": "application/json" } },
);
}
return realFetch(input, init);
};
const { Agent, JsonlLocalAgentStore } = await import("@cursor/sdk");
const store = new JsonlLocalAgentStore("./store");
const agent = await Agent.create({
apiKey: process.env.CURSOR_API_KEY,
model: { id: "claude-sonnet-4-6" },
mode: "agent",
local: { cwd: "./empty-dir", store, settingSources: [] },
});
const run = await agent.send("Reply with exactly: ok");
const result = await run.wait();
console.log(result);
// -> { status: "error", result: undefined } — NO error detail
// ./store/runs.ndjson meanwhile holds the real message in the run's `error` field.
Defect #3 reproduces with the same injection in a process that has no unhandledRejection handler: the process crashes.
Expected behavior
RunResultexposes the error code/message that is already written to the run store.- Auth rejections on the streaming run path trigger the same re-auth/rebuild recovery that unary calls get.
- A failed exchange rejects the
Agent.create/sendpromise instead of escaping as an unhandled rejection. - Guidance on diagnosing this class of rejection under privacy mode, where server-side request lookup isn’t available — ideally an SDK debug/verbose mode that logs the transport-level status and error body of rejected runs.
OS
macOS 26.5.2 (25F84), Apple Silicon. The same service also runs in node:22-alpine Linux containers.
Version info
@cursor/sdk: failures observed on 1.0.19 and 1.0.22; investigation performed on 1.0.22; since upgraded to 1.0.23- Node.js v22.22.3
- Local agents (
mode: "agent",local: {...}), auth via dashboard user API key, privacy mode enabled
If AI issue, what model
claude-sonnet-4-6 (explicit model id). Not model-related — failures occur before any model tokens are consumed.
Additional info
- Failure windows (UTC): 2026-07-02 ~15:40–19:48; 2026-07-03 ~07:48–08:03 and ~13:34.
- All failed runs have
requestIdset,durationMs600–3000ms, empty usage (we can share ~60 requestIds if useful despite privacy mode). - Heavy usage (~1.7M input tokens in 20 minutes) immediately preceded the 07-03 morning window, in case per-key throttling is relevant — though the dashboard shows us under quota.
- Failure phenomenology: within a window, a first attempt fails in ~1.6–3s and immediate retries in ~0.6–0.9s; new
Agent.create()sessions fail identically to resumed ones; warm-up-style sends after a process restart succeeded whenever they happened to fall outside a window. - Related: the
run.wait()error-swallowing gap was previously acknowledged by staff in thread 164248 (named models failing silently when over limits).
Does this stop you from using Cursor
Sometimes - I can sometimes use Cursor