Skip to content

Server exits when Cursor abandoned-run recovery cancels a run ("ConnectError: write after end") #27694

Closed

Description

@joveteo

What happened

A Cursor thread (claude-opus-5-5 via the Cursor harness) stopped mid-run. The user believes the Cursor account ran out of tokens. Shortly after, the whole T3 server process exited with this error on stderr:

Image
ConnectError: [aborted] write after end
  rawMessage: 'write after end', code: 10,
  cause: Error [ERR_STREAM_WRITE_AFTER_END]: write after end

Diagnosis

The crash happens in the startup recovery path, when T3 cancels an abandoned Cursor run.

  1. The server restarted (16:05:33 UTC) and resumed the interrupted Cursor turn on thread 1423ffbd… with "Continue where you left off".
  2. agent.send failed with already has active run. The recovery in CursorAgentSdk.ts (isActiveRunConflict → Agent.listRuns(..., { limit: 1 })) found the orphaned run. It was the agent's only run, so limit: 1 happened to return it. Recovery then called Agent.cancelRun(activeRun.id, runOptions) (packages/provider-cursor/src/server/CursorAgentSdk.ts:497).
  3. Provider log: run.cancel run-f74057a9… at 16:05:37.559Z. The retried run.start then succeeded (run.started run-6754ccbc… at 16:05:41.662Z). The server trace stops at 16:05:41.70Z, and the process is gone.
  4. Inside @cursor/sdk (dist/cjs/126.js), the cancel writes to a Connect stream that has already ended. That promise is not awaited by anything T3 holds, so it becomes an unhandled rejection.
  5. The guard in packages/provider-cursor/src/server/CursorSdk.ts (onUnhandledRejection) only passes isCursorShellSpawnFailure and isCursorClosedStreamWrite (name === "WriteIterableClosedError"). This error is a ConnectError with code 10 and cause ERR_STREAM_WRITE_AFTER_END, so the guard falls through to console.error(reason); process.exit(1).

This looks like the same SDK pattern the existing WriteIterableClosedError guard covers (a final write to a closed stream from an unawaited promise), arriving through a different error class.

Interaction with #24498 / PR #24540: today the limit: 1 lookup usually misses the stuck run, so cancelRun rarely executes. That is why most users see #24498 (thread stuck) and not this crash. PR #24540 makes recovery find the stuck run reliably, so this cancelRun path, and this crash, will run far more often once it lands. The two should probably be fixed together.

The server also exited earlier, at 16:03:59 UTC, about 1 s after a new run started on the same thread. No stderr is available for that exit, so I can't confirm it was the same error.

Steps to reproduce

Seen once on this machine. These are the steps that led to it; not yet confirmed as a deterministic repro:

  1. Start a Cursor-provider thread and send a turn that runs for a while.
  2. While the run is in progress, kill the T3 server so the run is orphaned in the Cursor SDK local store. Make sure this is the agent's first run, so listRuns({ limit: 1 }) returns it.
  3. Restart the server. Startup recovery resumes the turn, hits already has active run, finds the run, and calls Agent.cancelRun.
  4. A few seconds later the server exits with ConnectError: [aborted] write after end.

Version

0.1.6-nightly.20261030.3917 (server, t3 serve --tailscale-serve), @cursor/sdk 1.0.38. Still present on main as of 2026-10-31: the CursorSdk.ts guard is unchanged.

Environment

Raspberry Pi 5, Linux 6.18.50 arm64, Node v26.12.1, provider cursor (model claude-opus-5-5)

Evidence

# stderr at crash (paths redacted)
ConnectError: [aborted] write after end
    at ConnectError.from (~/.t3/runtime/versions/0.1.6-nightly.20261030.3917/node_modules/@connectrpc/connect/dist/cjs/connect-error.js:74:20)
    at i (.../node_modules/@cursor/sdk/dist/cjs/126.js:1:749)
    at .../node_modules/@cursor/sdk/dist/cjs/126.js:1:13906
    at process.processTicksAndRejections (node:internal/process/task_queues:89:21) {
  rawMessage: 'write after end',
  code: 10,
  metadata: Headers {},
  details: [],
  cause: Error [ERR_STREAM_WRITE_AFTER_END]: write after end
      at _write (node:internal/streams/writable:491:11)
      at Writable.write (node:internal/streams/writable:512:10)
      at .../node_modules/@cursor/sdk/dist/cjs/126.js:1:13835
      at process.processTicksAndRejections (node:internal/process/task_queues:104:5) {
    code: 'ERR_STREAM_WRITE_AFTER_END'
  }
}

# provider event log, thread 1423ffbd… (UTC)
16:03:54.338Z outgoing run.start
16:03:58.998Z incoming run.started run-f74057a9…
   (server exits ~16:03:59, restarts 16:05:33)
16:05:35.497Z outgoing agent.open
16:05:37.557Z outgoing run.start   "Continue where you left off."
16:05:37.559Z outgoing run.cancel  run-f74057a9…      <- abandoned-run recovery
16:05:41.662Z incoming run.started run-6754ccbc…
   (last trace span 16:05:41.70Z, then process exits)

Related issues

#24498 / PR #24540: same recovery path. That issue covers the run lookup missing the stuck run, which leaves the thread stuck. This issue covers what happens when the lookup does find it: cancelRun crashes the whole server. Not a duplicate.

Fix applied or workaround

None applied. Restarting the server brings it back, but then the affected threads hit #24498 and stay stuck. Starting a new thread works.

Filed by

Claude Code (claude-opus-5-5) via t3 triage

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