Idle local agent gRPC connection returns `ERROR_NOT_LOGGED_IN` (AuthenticationError) instead of NetworkError after ~15 minutes

Where does the bug appear (feature/product)?

Cursor SDK

Describe the Bug

When a local SDKAgent instance created via Agent.create() sits idle for approximately 15 minutes, the next agent.send() call fails with ERROR_NOT_LOGGED_IN (gRPC status code 16 / UNAUTHENTICATED). The error is surfaced as an AuthenticationError with isRetryable: false.

This is a misclassification. The API key is valid – creating a fresh agent via Agent.create() or Agent.resume() with the same key succeeds immediately. The underlying issue is a stale gRPC connection, which should surface as a NetworkError with isRetryable: true.

Because the error is classified as AuthenticationError, the SDK’s built-in enableAgentRetries transport retry (default: true) does not handle it. Consumers must implement their own retry logic to work around what should be an SDK-managed transport recovery.

Steps to Reproduce

  1. Create a local agent with a valid API key:
    const store = new JsonlLocalAgentStore("./agent-store");
    const agent = await Agent.create({
      apiKey: process.env.CURSOR_API_KEY,
      model: { id: "claude-sonnet-4-6" },
      mode: "agent",
      local: { cwd: "/path/to/repo", store },
    });
    
  2. Send a message successfully: const run = await agent.send("Hello"); await run.wait();
  3. Wait ~15 minutes with no activity on the agent
  4. Send another message: const run2 = await agent.send("Follow-up"); await run2.wait();
  5. Observe: run2.wait() resolves with { status: "error" } containing ERROR_NOT_LOGGED_IN
  6. Alternatively, the SDK throws AuthenticationError with message [unauthenticated] Error

Expected Behavior

  • The SDK should either transparently reconnect (since enableAgentRetries is true and this is a transport-level issue), or
  • Surface the error as NetworkError with isRetryable: true so consumer retry logic can handle it correctly
  • The error should NOT be AuthenticationError since the API key is valid

Operating System

MacOS

Version Information

  • @cursor/sdk: 1.0.19
  • Node.js: v22.x
  • OS: macOS (darwin 25.5.0) – also expected on Linux containers
  • Agent type: local (not cloud)
  • Store: JsonlLocalAgentStore
  • enableAgentRetries: default (true)

Additional Information

  • The gRPC status code 16 (UNAUTHENTICATED) from the Cursor backend is the source of the misclassification. The server-side connection cleanup triggers this code rather than UNAVAILABLE (14) or DEADLINE_EXCEEDED (4).
  • Agent.resume(agentId) with the same API key succeeds immediately after the failure, confirming the key is valid and only the connection is stale.
  • In a long-running server process (e.g., Fastify service acting as an AI agent pod), agents frequently go idle between user interactions. This pattern reliably triggers the bug after ~15 minutes.
  • Workaround: catch the error, evict the cached agent, call Agent.resume(agentId) to get a fresh connection with preserved conversation state, and retry the send.

Does this stop you from using Cursor

Sometimes - I can sometimes use Cursor

Hi Yaakov, thanks for such a thorough report. The repro steps, version details, and your own analysis made this very easy to confirm.

You have it exactly right, and we have confirmed the behavior on our side. This is a misclassification, not anything wrong with your API key. When a local agent sits idle, the short lived access token the SDK exchanges from your API key lapses, and the next agent.send() comes back from the server as an unauthenticated end of stream. Today that path maps to a terminal AuthenticationError with isRetryable: false, so the built in enableAgentRetries transport recovery never gets a chance to act on it. That is also why a fresh Agent.create() or Agent.resume() with the same key succeeds immediately: it establishes a new connection and re-exchanges the token.

Your workaround is the right one to stay on for now, and it is what we would recommend as well:

  1. Catch the error from send() / wait().
  2. Evict the cached agent handle.
  3. Call Agent.resume(agentId) to get a fresh connection (this preserves your conversation state).
  4. Retry the send.

On the fix: we have logged this internally as a confirmed issue, and we agree with the expected behavior you outlined. Ideally the SDK either reconnects transparently or surfaces this as a retryable error so your own retry logic can act on it, rather than failing as a terminal AuthenticationError. I do not have a timeline to share yet, but I will follow up here as it moves.