Where does the bug appear (feature/product)?
Cursor SDK
Describe the Bug
When the bridge subprocess catches a CursorSdkError whose class name isn’t in SDK_ERROR_NAME_TO_CONNECT_CODE (and whose .code isn’t in SDK_ERROR_CODE_TO_CONNECT_CODE), it returns the error untranslated. Connect-node’s default adapter then collapses it to Code.Internal, surfacing as internal: internal error in any non-JS bridge consumer. The real error class, message, and stack are unrecoverable from outside the bridge process — making certain failure modes (notably “agent already has active run”) opaque and unactionable for downstream clients like cursor-sdk Python.
The file responsible (cursor_sdk/_vendor/bridge/dist/sdk-error-interceptor.js) acknowledges this design dependency in its own comment:
“Without this translation, connect-node’s adapter collapses every non-
ConnectErrorintoCode.Internal, hiding the structured code from SDK consumers.”
…but the mapping table is incomplete.
Steps to Reproduce
Concretely: agent.send() throws UnknownAgentError: Agent <id> already has active run server-side. The Python client receives:
cursor_sdk.errors.InternalServerError: internal: internal error
The structured error name (UnknownAgentError), the real message (already has active run), the JS stack pointing inside StoreBackedAgent, and any .requestId/.helpUrl metadata are all lost.
Minimal Reproduction (Python, against the bridge)
"""
Reproduces: bridge collapses UnknownAgentError to "internal: internal error".
Triggers the active-run state by issuing a second `send()` before the first
run finishes, which causes `StoreBackedAgent` to throw `UnknownAgentError`
with the message "Agent <id> already has active run".
Usage:
CURSOR_API_KEY=crsr_xxx uv run python repro.py
"""
import asyncio
import os
from cursor_sdk import AsyncAgent, AsyncClient
from cursor_sdk.types import AgentOptions, LocalAgentOptions
async def main() -> None:
api_key = os.environ.get("CURSOR_API_KEY")
if not api_key:
raise SystemExit("CURSOR_API_KEY required")
async with await AsyncClient.launch_bridge(
workspace=os.getcwd(),
allow_api_key_env_fallback=True,
) as client:
async with await AsyncAgent.create(
AgentOptions(
model="composer-2.5",
local=LocalAgentOptions(cwd=os.getcwd()),
),
client=client,
) as agent:
# Kick off a long-running first send but don't await its stream.
run1 = await agent.send(
"Read every file in this repo, then summarise in 5000 words."
)
# Give the bridge a moment to register the run as 'running'.
await asyncio.sleep(0.5)
# Second send while run1 is still in flight.
try:
await agent.send("ignore that, write hello world")
except Exception as exc:
print(f"caught: {type(exc).__name__}: {exc}")
# Expected: AgentBusyError (or UnknownAgentError) with the
# real message "Agent <id> already has active run".
# Actual: InternalServerError: internal: internal error
if __name__ == "__main__":
asyncio.run(main())
Reproduction Steps
pip install cursor-sdk
CURSOR_API_KEY=crsr_xxx python repro.py
Observe: the caught exception is InternalServerError: internal: internal error. The fact that the failure was due to an active run on this agent is not recoverable from the exception alone.
Expected Behavior
When the SDK throws an UnknownAgentError (or any structured CursorSdkError subclass) inside an RPC handler, the bridge should translate it to a meaningful Connect code so downstream clients receive the real error class, code, and message — not Code.Internal: internal: internal error.
Operating System
Linux
Version Information
cursor-sdkPython: v0.1.6- Bundled
@cursor/sdk(Node, inside_vendor/bridge/node_modules/): v1.0.15 cursor-sdk-bridgeserver: ships with v0.1.6- Node: v22.x (bundled)
- OS: Linux
Additional Information
Diagnostic Output (with bridge stderr patched to log raw errors)
caught: InternalServerError: internal: internal error
Inside the bridge process (only visible if you patch sdk-error-interceptor.js to log to stderr):
{
"name": "UnknownAgentError",
"message": "Agent agent-0c3974a4-... already has active run",
"code": undefined,
"stack": "UnknownAgentError: Agent agent-0c3974a4-... already has active run
at g (.../node_modules/@cursor/sdk/dist/esm/index.js:8:1078869)
at StoreBackedAgent.<anonymous> (.../node_modules/@cursor/sdk/dist/esm/index.js:8:6189282)"
}
Key observations:
- The JS error has
.name = "UnknownAgentError"but.codeis undefined or unset, so neither lookup table insdk-error-interceptor.jsmatches. UnknownAgentErroris also misleading as a class name — the message is about an active run, not an unknown agent. Semantically this is closer toAgentBusyError(which IS in the name map).- Because the error escapes the interceptor unchanged, connect-node collapses it to
Code.Internal, and downstream clients lose the structured information.
Root Cause
cursor_sdk/_vendor/bridge/dist/sdk-error-interceptor.js:
const SDK_ERROR_NAME_TO_CONNECT_CODE = {
AuthenticationError: Code.Unauthenticated,
ConfigurationError: Code.InvalidArgument,
AgentBusyError: Code.FailedPrecondition,
RateLimitError: Code.ResourceExhausted,
};
Several CursorSdkError subclasses are absent from this map and from SDK_ERROR_CODE_TO_CONNECT_CODE:
UnknownAgentErrorNotFoundErrorBadRequestErrorIntegrationNotConnectedErrorInternalServerErrorAPITimeoutErrorNetworkErrorUnsupportedRunOperationErrorPermissionDeniedError(inherits fromAuthenticationErrorbuterror.nameis"PermissionDeniedError", not"AuthenticationError", so it doesn’t match)
Anything thrown with one of those .name values and no recognised .code is silently collapsed.
Workaround
Patch the bridge’s sdk-error-interceptor.js post-install to add the missing entries:
const SDK_ERROR_NAME_TO_CONNECT_CODE = {
AuthenticationError: Code.Unauthenticated,
PermissionDeniedError: Code.PermissionDenied,
ConfigurationError: Code.InvalidArgument,
BadRequestError: Code.InvalidArgument,
UnsupportedRunOperationError: Code.FailedPrecondition,
AgentBusyError: Code.FailedPrecondition,
IntegrationNotConnectedError: Code.FailedPrecondition,
RateLimitError: Code.ResourceExhausted,
NotFoundError: Code.NotFound,
UnknownAgentError: Code.NotFound, // or FailedPrecondition for the "active run" case
APITimeoutError: Code.DeadlineExceeded,
NetworkError: Code.Unavailable,
InternalServerError: Code.Internal, // preserves message instead of collapsing
};
This is brittle — it gets wiped on every pip install --upgrade cursor-sdk — but is the only way for non-JS bridge consumers to surface meaningful errors today.
Feature Request / Suggested Fix
One (or both) of:
-
Add the missing entries to
SDK_ERROR_NAME_TO_CONNECT_CODEupstream, with sensible Connect-code mappings. The list above is a reasonable starting point. -
Default fallback that preserves the error name/message rather than collapsing to
Code.Internal: internal: internal error. e.g. if no mapping matches but the error is an instance ofCursorSdkError, wrap it asnew ConnectError(${error.name}: ${error.message}, Code.Internal, ...)so at least the class name and message survive. -
Audit
UnknownAgentErrorusage — throwing it with the message “Agent X already has active run” is misleading; that condition is closer toAgentBusyErrorand would benefit from a dedicated class or.codevalue.
Does this stop you from using Cursor
No - Cursor works, but with this issue