cursor-opencode-provider 0.2.2 → 0.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -12
- package/dist/activity.d.ts +15 -0
- package/dist/activity.js +76 -0
- package/dist/context/build.js +0 -7
- package/dist/errors.d.ts +59 -0
- package/dist/errors.js +199 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +4 -0
- package/dist/language-model.d.ts +47 -4
- package/dist/language-model.js +638 -184
- package/dist/models.d.ts +7 -0
- package/dist/models.js +259 -79
- package/dist/plugin.js +35 -2
- package/dist/protocol/exec-variants.d.ts +24 -0
- package/dist/protocol/exec-variants.js +55 -0
- package/dist/protocol/messages.js +137 -24
- package/dist/protocol/request.d.ts +0 -2
- package/dist/protocol/request.js +0 -8
- package/dist/protocol/struct.js +1 -1
- package/dist/protocol/tool-call-bridge.js +2 -0
- package/dist/protocol/tools.d.ts +14 -1
- package/dist/protocol/tools.js +312 -27
- package/dist/session.d.ts +115 -8
- package/dist/session.js +362 -31
- package/dist/shell-timeout.d.ts +50 -0
- package/dist/shell-timeout.js +179 -0
- package/dist/transport/connect.d.ts +37 -1
- package/dist/transport/connect.js +679 -115
- package/package.json +6 -1
|
@@ -4,6 +4,7 @@ import { createCursorChecksumHeader } from "../protocol/checksum.js";
|
|
|
4
4
|
import { getDeviceIds } from "../protocol/device-id.js";
|
|
5
5
|
import { resolveClientVersion } from "../protocol/client-version.js";
|
|
6
6
|
import { trace } from "../debug.js";
|
|
7
|
+
import { CursorLocalCancellationError, CursorProtocolError, CursorTransportError, cursorGrpcError, cursorHttpError, errorCode, CursorProviderError, } from "../errors.js";
|
|
7
8
|
import http2 from "node:http2";
|
|
8
9
|
const API_BASE = `https://${CURSOR_API_HOST}`;
|
|
9
10
|
function resolveApiBaseURL(options) {
|
|
@@ -12,16 +13,21 @@ function resolveApiBaseURL(options) {
|
|
|
12
13
|
trace("connect.ts module loaded");
|
|
13
14
|
export function buildBaseHeaders(token, clientVersion, extra) {
|
|
14
15
|
const { machineId, macMachineId } = getDeviceIds();
|
|
15
|
-
|
|
16
|
+
const headers = {
|
|
16
17
|
authorization: `Bearer ${token}`,
|
|
17
18
|
"connect-protocol-version": CONNECT_PROTOCOL_VERSION,
|
|
18
19
|
"x-cursor-client-type": "cli",
|
|
19
20
|
"x-cursor-client-version": clientVersion,
|
|
20
21
|
"x-cursor-checksum": createCursorChecksumHeader(machineId, macMachineId),
|
|
21
22
|
"x-ghost-mode": "true",
|
|
22
|
-
"x-request-id": crypto.randomUUID(),
|
|
23
23
|
...extra,
|
|
24
24
|
};
|
|
25
|
+
for (const key of Object.keys(headers)) {
|
|
26
|
+
if (key.toLowerCase() === "x-request-id")
|
|
27
|
+
delete headers[key];
|
|
28
|
+
}
|
|
29
|
+
headers["x-request-id"] = crypto.randomUUID();
|
|
30
|
+
return headers;
|
|
25
31
|
}
|
|
26
32
|
// ── Unary (AvailableModels) ──
|
|
27
33
|
export async function unaryAvailableModels(token, options = {}) {
|
|
@@ -29,30 +35,40 @@ export async function unaryAvailableModels(token, options = {}) {
|
|
|
29
35
|
const url = `${base}/aiserver.v1.AiService/AvailableModels`;
|
|
30
36
|
const clientVersion = await resolveClientVersion();
|
|
31
37
|
const headers = buildBaseHeaders(token, clientVersion, options.headers);
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
38
|
+
let res;
|
|
39
|
+
try {
|
|
40
|
+
res = await fetch(url, {
|
|
41
|
+
method: "POST",
|
|
42
|
+
headers: {
|
|
43
|
+
...headers,
|
|
44
|
+
"content-type": "application/json",
|
|
45
|
+
accept: "application/json",
|
|
46
|
+
},
|
|
47
|
+
// Request parameterized effort/context/fast variants like Cursor IDE.
|
|
48
|
+
body: JSON.stringify({
|
|
49
|
+
includeLongContextModels: true,
|
|
50
|
+
useModelParameters: true,
|
|
51
|
+
useCloudAgentEffortModes: true,
|
|
52
|
+
}),
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
catch (cause) {
|
|
56
|
+
throw new CursorTransportError("AvailableModels network request failed", {
|
|
57
|
+
transient: true,
|
|
58
|
+
replaySafe: true,
|
|
59
|
+
code: errorCode(cause),
|
|
60
|
+
cause,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
51
63
|
if (!res.ok) {
|
|
52
|
-
|
|
53
|
-
|
|
64
|
+
throw await cursorHttpResponseError("AvailableModels failed:", res);
|
|
65
|
+
}
|
|
66
|
+
try {
|
|
67
|
+
return (await res.json());
|
|
68
|
+
}
|
|
69
|
+
catch (cause) {
|
|
70
|
+
throw new CursorProtocolError("AvailableModels returned malformed JSON", { cause });
|
|
54
71
|
}
|
|
55
|
-
return (await res.json());
|
|
56
72
|
}
|
|
57
73
|
// ── Unary (GetServerConfig) ──
|
|
58
74
|
/**
|
|
@@ -104,46 +120,113 @@ export async function fetchAgentUrl(token, options = {}) {
|
|
|
104
120
|
// Match Cursor CLI: GetServerConfig uses base headers only — session headers belong on Run.
|
|
105
121
|
const headers = buildBaseHeaders(token, clientVersion);
|
|
106
122
|
trace(`GetServerConfig POST ${url}`);
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
123
|
+
let res;
|
|
124
|
+
try {
|
|
125
|
+
res = await fetch(url, {
|
|
126
|
+
method: "POST",
|
|
127
|
+
headers: {
|
|
128
|
+
...headers,
|
|
129
|
+
"content-type": "application/json",
|
|
130
|
+
accept: "application/json",
|
|
131
|
+
},
|
|
132
|
+
body: JSON.stringify({ telem_enabled: options.telemetryEnabled ?? false }),
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
catch (cause) {
|
|
136
|
+
throw new CursorTransportError("GetServerConfig network request failed", {
|
|
137
|
+
transient: true,
|
|
138
|
+
replaySafe: true,
|
|
139
|
+
code: errorCode(cause),
|
|
140
|
+
cause,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
116
143
|
if (!res.ok) {
|
|
117
|
-
|
|
118
|
-
|
|
144
|
+
throw await cursorHttpResponseError("GetServerConfig failed:", res);
|
|
145
|
+
}
|
|
146
|
+
let body;
|
|
147
|
+
try {
|
|
148
|
+
body = (await res.json());
|
|
149
|
+
}
|
|
150
|
+
catch (cause) {
|
|
151
|
+
throw new CursorProtocolError("GetServerConfig returned malformed JSON", { cause });
|
|
119
152
|
}
|
|
120
|
-
const body = (await res.json());
|
|
121
153
|
const cfg = body.agentUrlConfig;
|
|
122
154
|
const raw = cfg?.agentnUrl || cfg?.agentUrl;
|
|
123
155
|
const normalized = normalizeAgentRunOrigin(raw);
|
|
124
156
|
trace(`GetServerConfig reply: agentnUrl=${cfg?.agentnUrl ?? "<missing>"} ` +
|
|
125
157
|
`agentUrl=${cfg?.agentUrl ?? "<missing>"} → ${normalized ?? "<invalid>"}`);
|
|
126
158
|
if (!normalized) {
|
|
127
|
-
throw new
|
|
159
|
+
throw new CursorProtocolError(raw
|
|
128
160
|
? "GetServerConfig returned an invalid Cursor agent URL"
|
|
129
161
|
: "GetServerConfig response missing agentUrlConfig.agentnUrl");
|
|
130
162
|
}
|
|
131
163
|
return normalized;
|
|
132
164
|
}
|
|
165
|
+
export class CursorRunInterruptedError extends CursorTransportError {
|
|
166
|
+
constructor(message = "Cursor Run ended before turn_ended", options) {
|
|
167
|
+
super(message, {
|
|
168
|
+
transient: true,
|
|
169
|
+
replaySafe: true,
|
|
170
|
+
cause: options?.cause,
|
|
171
|
+
});
|
|
172
|
+
this.name = "CursorRunInterruptedError";
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
export function cursorRunTerminationError(input) {
|
|
176
|
+
const headers = input.responseHeaders ?? {};
|
|
177
|
+
const trailers = input.responseTrailers ?? {};
|
|
178
|
+
if (input.streamError) {
|
|
179
|
+
return new CursorRunInterruptedError(`Cursor Run transport interrupted: ${input.streamError.message}`, { cause: input.streamError });
|
|
180
|
+
}
|
|
181
|
+
if (input.responseStatus !== 0 && input.responseStatus !== 200) {
|
|
182
|
+
return cursorRunHttpError(input.responseStatus, headers);
|
|
183
|
+
}
|
|
184
|
+
const grpcStatus = trailers["grpc-status"] ?? headers["grpc-status"];
|
|
185
|
+
if (grpcStatus !== undefined && String(grpcStatus) !== "0") {
|
|
186
|
+
return cursorRunGrpcError(String(grpcStatus), headers, trailers);
|
|
187
|
+
}
|
|
188
|
+
return new CursorRunInterruptedError();
|
|
189
|
+
}
|
|
190
|
+
function withErrorMessageSuffix(error, suffix) {
|
|
191
|
+
if (suffix)
|
|
192
|
+
error.message += suffix;
|
|
193
|
+
return error;
|
|
194
|
+
}
|
|
195
|
+
async function cursorHttpResponseError(operation, res) {
|
|
196
|
+
const text = await res.text().catch(() => "");
|
|
197
|
+
return withErrorMessageSuffix(cursorHttpError(operation, res.status), `${res.statusText ? ` ${res.statusText}` : ""}${text ? ` - ${text.slice(0, 200)}` : ""}`);
|
|
198
|
+
}
|
|
199
|
+
function cursorRunHttpError(statusCode, headers) {
|
|
200
|
+
return withErrorMessageSuffix(cursorHttpError("Cursor Run failed by remote:", statusCode), ` ${JSON.stringify(stripPseudo(headers))}`);
|
|
201
|
+
}
|
|
202
|
+
function cursorRunGrpcError(grpcStatus, headers, trailers) {
|
|
203
|
+
const message = trailers["grpc-message"] ?? headers["grpc-message"];
|
|
204
|
+
return withErrorMessageSuffix(cursorGrpcError("Cursor Run failed by remote:", grpcStatus), message === undefined ? "" : `: ${message}`);
|
|
205
|
+
}
|
|
133
206
|
// Cache http2 sessions keyed by origin so a custom baseURL never reuses a
|
|
134
207
|
// connection opened to the default agent host — and vice versa.
|
|
135
208
|
const _http2Sessions = new Map();
|
|
136
|
-
|
|
209
|
+
const _http2SessionCreatedAt = new WeakMap();
|
|
210
|
+
const _http2SessionListenerCleanup = new WeakMap();
|
|
211
|
+
// Validation and connect work for the same origin shares one Promise.
|
|
137
212
|
const _http2Connecting = new Map();
|
|
138
213
|
// Bound the time we'll wait for the initial connect. Without this, a dead or
|
|
139
214
|
// unreachable host (DNS failure, network partition) hangs the provider forever
|
|
140
215
|
// because the 'connect' / 'error' event may never fire.
|
|
141
216
|
const CONNECT_TIMEOUT_MS = 15_000;
|
|
217
|
+
const DEFAULT_SESSION_PING_TIMEOUT_MS = 5_000;
|
|
218
|
+
const DEFAULT_READ_IDLE_MS = 120_000;
|
|
219
|
+
const WRITE_DRAIN_TIMEOUT_CODE = "CURSOR_WRITE_DRAIN_TIMEOUT";
|
|
220
|
+
const MAX_TIMER_MS = 2_147_483_647;
|
|
221
|
+
export const HTTP2_SESSION_MAX_AGE_MS = 15 * 60_000;
|
|
222
|
+
export function shouldReuseHttp2Session(state, createdAt, now = Date.now()) {
|
|
223
|
+
return !state.destroyed && !state.closed && now - createdAt < HTTP2_SESSION_MAX_AGE_MS;
|
|
224
|
+
}
|
|
142
225
|
/** Resolve the HTTP/2 connect origin for a Run stream (exported for tests). */
|
|
143
226
|
export function resolveAgentOrigin(baseURL) {
|
|
144
227
|
const origin = normalizeAgentRunOrigin(baseURL);
|
|
145
228
|
if (!origin) {
|
|
146
|
-
throw new
|
|
229
|
+
throw new CursorProtocolError("Cursor Run stream requires an allowlisted Cursor agent base URL");
|
|
147
230
|
}
|
|
148
231
|
return origin;
|
|
149
232
|
}
|
|
@@ -151,62 +234,250 @@ function dropSession(origin, session) {
|
|
|
151
234
|
if (_http2Sessions.get(origin) === session)
|
|
152
235
|
_http2Sessions.delete(origin);
|
|
153
236
|
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
const
|
|
157
|
-
|
|
158
|
-
|
|
237
|
+
/** Test cleanup for local HTTP/2 fixtures; production sessions stay process-cached. */
|
|
238
|
+
export function closeCachedHttp2SessionsForTests() {
|
|
239
|
+
for (const session of _http2Sessions.values()) {
|
|
240
|
+
_http2SessionListenerCleanup.get(session)?.();
|
|
241
|
+
try {
|
|
242
|
+
session.destroy();
|
|
243
|
+
}
|
|
244
|
+
catch { /* already closed */ }
|
|
159
245
|
}
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
246
|
+
_http2Sessions.clear();
|
|
247
|
+
_http2Connecting.clear();
|
|
248
|
+
}
|
|
249
|
+
function timeoutMs(name, value, fallback) {
|
|
250
|
+
const resolved = value ?? fallback;
|
|
251
|
+
if (!Number.isSafeInteger(resolved) || resolved <= 0 || resolved > MAX_TIMER_MS) {
|
|
252
|
+
throw new CursorProtocolError(`${name} must be a positive integer no greater than ${MAX_TIMER_MS}`);
|
|
253
|
+
}
|
|
254
|
+
return resolved;
|
|
255
|
+
}
|
|
256
|
+
function toTransportError(value, fallback) {
|
|
257
|
+
if (value instanceof CursorTransportError)
|
|
258
|
+
return value;
|
|
259
|
+
return new CursorTransportError(fallback, {
|
|
260
|
+
transient: true,
|
|
261
|
+
replaySafe: true,
|
|
262
|
+
code: errorCode(value),
|
|
263
|
+
cause: value,
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
function installSessionInvalidation(origin, session) {
|
|
267
|
+
const socket = session.socket;
|
|
268
|
+
let cleaned = false;
|
|
269
|
+
let removeSocketListeners;
|
|
270
|
+
const cleanup = () => {
|
|
271
|
+
if (cleaned)
|
|
272
|
+
return;
|
|
273
|
+
cleaned = true;
|
|
274
|
+
session.removeListener("close", onSessionClose);
|
|
275
|
+
session.removeListener("goaway", onSessionGoaway);
|
|
276
|
+
session.removeListener("error", onSessionError);
|
|
277
|
+
removeSocketListeners?.();
|
|
278
|
+
removeSocketListeners = undefined;
|
|
279
|
+
_http2SessionListenerCleanup.delete(session);
|
|
280
|
+
};
|
|
281
|
+
const onSessionClose = () => {
|
|
282
|
+
trace(`h2 session closed: origin=${origin}`);
|
|
283
|
+
dropSession(origin, session);
|
|
284
|
+
cleanup();
|
|
285
|
+
};
|
|
286
|
+
const onSessionGoaway = (errorCode, lastStreamID) => {
|
|
287
|
+
trace(`h2 session GOAWAY: origin=${origin} errorCode=${errorCode} lastStreamID=${lastStreamID}`);
|
|
288
|
+
// Existing streams may drain, but future Runs must use a fresh session.
|
|
289
|
+
dropSession(origin, session);
|
|
290
|
+
};
|
|
291
|
+
const onSessionError = (error) => {
|
|
292
|
+
trace(`h2 session error: origin=${origin} err=${error.message}`);
|
|
293
|
+
dropSession(origin, session);
|
|
294
|
+
};
|
|
295
|
+
const onSocketEnd = () => {
|
|
296
|
+
trace(`h2 socket ended: origin=${origin}`);
|
|
297
|
+
dropSession(origin, session);
|
|
298
|
+
};
|
|
299
|
+
const onSocketClose = () => {
|
|
300
|
+
trace(`h2 socket closed: origin=${origin}`);
|
|
301
|
+
dropSession(origin, session);
|
|
302
|
+
};
|
|
303
|
+
session.once("close", onSessionClose);
|
|
304
|
+
session.on("goaway", onSessionGoaway);
|
|
305
|
+
// Keep an error listener until close so teardown errors cannot become an
|
|
306
|
+
// unhandled EventEmitter error.
|
|
307
|
+
session.on("error", onSessionError);
|
|
308
|
+
if (socket) {
|
|
309
|
+
socket.once("end", onSocketEnd);
|
|
310
|
+
socket.once("close", onSocketClose);
|
|
311
|
+
// Http2Session.socket is a lifecycle-bound proxy. Looking up methods after
|
|
312
|
+
// detach throws ERR_HTTP2_SOCKET_UNBOUND, so bind removers while attached.
|
|
313
|
+
const removeEndListener = socket.removeListener.bind(socket, "end", onSocketEnd);
|
|
314
|
+
const removeCloseListener = socket.removeListener.bind(socket, "close", onSocketClose);
|
|
315
|
+
removeSocketListeners = () => {
|
|
316
|
+
try {
|
|
317
|
+
removeEndListener();
|
|
318
|
+
}
|
|
319
|
+
catch { /* teardown is best-effort */ }
|
|
320
|
+
try {
|
|
321
|
+
removeCloseListener();
|
|
322
|
+
}
|
|
323
|
+
catch { /* teardown is best-effort */ }
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
_http2SessionListenerCleanup.set(session, cleanup);
|
|
327
|
+
}
|
|
328
|
+
/** Node-runtime regression hook; production callers use getSession(). */
|
|
329
|
+
export function installSessionInvalidationForTests(origin, session) {
|
|
330
|
+
installSessionInvalidation(origin, session);
|
|
331
|
+
}
|
|
332
|
+
function invalidateSession(origin, session) {
|
|
333
|
+
dropSession(origin, session);
|
|
334
|
+
_http2SessionListenerCleanup.get(session)?.();
|
|
335
|
+
try {
|
|
336
|
+
session.destroy();
|
|
337
|
+
}
|
|
338
|
+
catch { /* already closed */ }
|
|
339
|
+
}
|
|
340
|
+
function validateCachedSession(session, pingTimeoutMs) {
|
|
341
|
+
if (session.destroyed || session.closed) {
|
|
342
|
+
return Promise.reject(new CursorTransportError("Cursor HTTP/2 cached session is already closed"));
|
|
343
|
+
}
|
|
344
|
+
return new Promise((resolve, reject) => {
|
|
345
|
+
let settled = false;
|
|
346
|
+
const finish = (error) => {
|
|
347
|
+
if (settled)
|
|
348
|
+
return;
|
|
349
|
+
settled = true;
|
|
350
|
+
clearTimeout(timer);
|
|
351
|
+
session.removeListener("close", onClosed);
|
|
352
|
+
session.removeListener("goaway", onGoaway);
|
|
353
|
+
session.removeListener("error", onError);
|
|
354
|
+
if (error)
|
|
355
|
+
reject(error);
|
|
356
|
+
else
|
|
357
|
+
resolve();
|
|
358
|
+
};
|
|
359
|
+
const onClosed = () => finish(new CursorTransportError("Cursor HTTP/2 cached session closed during ping"));
|
|
360
|
+
const onGoaway = () => finish(new CursorTransportError("Cursor HTTP/2 cached session received GOAWAY during ping"));
|
|
361
|
+
const onError = (error) => finish(toTransportError(error, "Cursor HTTP/2 cached session ping failed"));
|
|
362
|
+
const timer = setTimeout(() => {
|
|
363
|
+
finish(new CursorTransportError(`Cursor HTTP/2 cached session ping timed out after ${pingTimeoutMs}ms`));
|
|
364
|
+
}, pingTimeoutMs);
|
|
365
|
+
timer.unref?.();
|
|
366
|
+
session.once("close", onClosed);
|
|
367
|
+
session.once("goaway", onGoaway);
|
|
368
|
+
session.once("error", onError);
|
|
369
|
+
try {
|
|
370
|
+
const accepted = session.ping((error) => {
|
|
371
|
+
if (error)
|
|
372
|
+
finish(toTransportError(error, "Cursor HTTP/2 cached session ping failed"));
|
|
373
|
+
else
|
|
374
|
+
finish();
|
|
375
|
+
});
|
|
376
|
+
if (!accepted)
|
|
377
|
+
finish(new CursorTransportError("Cursor HTTP/2 cached session refused a health-check ping"));
|
|
378
|
+
}
|
|
379
|
+
catch (error) {
|
|
380
|
+
finish(toTransportError(error, "Cursor HTTP/2 cached session ping failed"));
|
|
381
|
+
}
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
function connectSession(origin) {
|
|
385
|
+
return new Promise((resolve, reject) => {
|
|
164
386
|
const session = http2.connect(origin);
|
|
387
|
+
let settled = false;
|
|
165
388
|
const cleanup = () => {
|
|
166
389
|
clearTimeout(timer);
|
|
167
390
|
session.removeListener("error", onError);
|
|
391
|
+
session.removeListener("close", onClose);
|
|
168
392
|
session.removeListener("connect", onConnect);
|
|
169
393
|
};
|
|
170
|
-
const
|
|
394
|
+
const fail = (error) => {
|
|
395
|
+
if (settled)
|
|
396
|
+
return;
|
|
397
|
+
settled = true;
|
|
171
398
|
cleanup();
|
|
172
|
-
_http2Connecting.delete(origin);
|
|
173
399
|
dropSession(origin, session);
|
|
174
400
|
try {
|
|
175
401
|
session.destroy();
|
|
176
402
|
}
|
|
177
403
|
catch { /* ignore */ }
|
|
178
|
-
reject(
|
|
404
|
+
reject(error);
|
|
179
405
|
};
|
|
406
|
+
const onError = (error) => fail(toTransportError(error, "Cursor HTTP/2 connection failed"));
|
|
407
|
+
const onClose = () => fail(new CursorTransportError(`HTTP/2 connection to ${origin} closed before connecting`));
|
|
180
408
|
const onConnect = () => {
|
|
409
|
+
if (settled)
|
|
410
|
+
return;
|
|
411
|
+
settled = true;
|
|
181
412
|
cleanup();
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
413
|
+
installSessionInvalidation(origin, session);
|
|
414
|
+
if (session.destroyed || session.closed) {
|
|
415
|
+
const error = new CursorTransportError(`HTTP/2 connection to ${origin} closed while connecting`);
|
|
416
|
+
invalidateSession(origin, session);
|
|
417
|
+
reject(error);
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
_http2SessionCreatedAt.set(session, Date.now());
|
|
188
421
|
_http2Sessions.set(origin, session);
|
|
422
|
+
trace(`h2 session connected: origin=${origin}`);
|
|
189
423
|
resolve(session);
|
|
190
424
|
};
|
|
191
425
|
const timer = setTimeout(() => {
|
|
192
|
-
|
|
193
|
-
_http2Connecting.delete(origin);
|
|
194
|
-
dropSession(origin, session);
|
|
195
|
-
try {
|
|
196
|
-
session.destroy();
|
|
197
|
-
}
|
|
198
|
-
catch { /* ignore */ }
|
|
199
|
-
reject(new Error(`HTTP/2 connect to ${origin} timed out after ${CONNECT_TIMEOUT_MS}ms`));
|
|
426
|
+
fail(new CursorTransportError(`HTTP/2 connect to ${origin} timed out after ${CONNECT_TIMEOUT_MS}ms`));
|
|
200
427
|
}, CONNECT_TIMEOUT_MS);
|
|
428
|
+
timer.unref?.();
|
|
201
429
|
session.on("error", onError);
|
|
202
|
-
session.
|
|
430
|
+
session.once("close", onClose);
|
|
431
|
+
session.once("connect", onConnect);
|
|
203
432
|
});
|
|
433
|
+
}
|
|
434
|
+
export function getSession(baseURL, options = {}) {
|
|
435
|
+
const origin = resolveAgentOrigin(baseURL);
|
|
436
|
+
const pingTimeoutMs = timeoutMs("Cursor provider pingTimeoutMs", options.pingTimeoutMs, DEFAULT_SESSION_PING_TIMEOUT_MS);
|
|
437
|
+
const inflight = _http2Connecting.get(origin);
|
|
438
|
+
if (inflight)
|
|
439
|
+
return inflight;
|
|
440
|
+
const promise = (async () => {
|
|
441
|
+
const existing = _http2Sessions.get(origin);
|
|
442
|
+
if (existing) {
|
|
443
|
+
const createdAt = _http2SessionCreatedAt.get(existing) ?? 0;
|
|
444
|
+
if (!shouldReuseHttp2Session(existing, createdAt)) {
|
|
445
|
+
trace(`h2 session rotate: origin=${origin} ageMs=${Math.max(0, Date.now() - createdAt)}`);
|
|
446
|
+
dropSession(origin, existing);
|
|
447
|
+
try {
|
|
448
|
+
existing.close();
|
|
449
|
+
}
|
|
450
|
+
catch { /* already closed */ }
|
|
451
|
+
}
|
|
452
|
+
else {
|
|
453
|
+
try {
|
|
454
|
+
await validateCachedSession(existing, pingTimeoutMs);
|
|
455
|
+
if (_http2Sessions.get(origin) === existing && !existing.destroyed && !existing.closed) {
|
|
456
|
+
trace(`h2 cached session ping ok: origin=${origin}`);
|
|
457
|
+
return existing;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
catch (error) {
|
|
461
|
+
trace(`h2 cached session ping failed: origin=${origin} err=${error.message}`);
|
|
462
|
+
invalidateSession(origin, existing);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
return connectSession(origin);
|
|
467
|
+
})();
|
|
204
468
|
_http2Connecting.set(origin, promise);
|
|
469
|
+
const cleanup = () => {
|
|
470
|
+
if (_http2Connecting.get(origin) === promise)
|
|
471
|
+
_http2Connecting.delete(origin);
|
|
472
|
+
};
|
|
473
|
+
promise.then(cleanup, cleanup);
|
|
205
474
|
return promise;
|
|
206
475
|
}
|
|
207
476
|
export async function bidiRunStream(token, options) {
|
|
477
|
+
const origin = resolveAgentOrigin(options.baseURL);
|
|
478
|
+
const readIdleMs = timeoutMs("Cursor provider readIdleMs", options.readIdleMs, DEFAULT_READ_IDLE_MS);
|
|
208
479
|
const [session, clientVersion] = await Promise.all([
|
|
209
|
-
getSession(options.baseURL),
|
|
480
|
+
getSession(options.baseURL, { pingTimeoutMs: options.pingTimeoutMs }),
|
|
210
481
|
resolveClientVersion(),
|
|
211
482
|
]);
|
|
212
483
|
const headers = {
|
|
@@ -225,10 +496,179 @@ export async function bidiRunStream(token, options) {
|
|
|
225
496
|
endStream: false,
|
|
226
497
|
});
|
|
227
498
|
let writable = true;
|
|
228
|
-
let
|
|
499
|
+
let locallyClosed = false;
|
|
500
|
+
let remotelyClosed = false;
|
|
501
|
+
let backpressured = false;
|
|
229
502
|
let responseStatus = 0;
|
|
230
503
|
let responseHeaders = {};
|
|
231
|
-
let
|
|
504
|
+
let responseTrailers = {};
|
|
505
|
+
let rawStreamError;
|
|
506
|
+
let streamFailure = null;
|
|
507
|
+
let terminalEvent;
|
|
508
|
+
let terminalSettlementScheduled = false;
|
|
509
|
+
const terminalListeners = new Set();
|
|
510
|
+
const inboundFrames = [];
|
|
511
|
+
const pendingChunks = [];
|
|
512
|
+
let inboundEnded = false;
|
|
513
|
+
let inboundFailure;
|
|
514
|
+
let inboundWaiter;
|
|
515
|
+
let readIdleTimer;
|
|
516
|
+
let inboundReaderStarted = false;
|
|
517
|
+
const abortSignal = options.signal;
|
|
518
|
+
let abortHandler;
|
|
519
|
+
const observedFailure = () => {
|
|
520
|
+
if (streamFailure)
|
|
521
|
+
return streamFailure;
|
|
522
|
+
if (responseStatus !== 0 && responseStatus !== 200) {
|
|
523
|
+
streamFailure = cursorRunHttpError(responseStatus, responseHeaders);
|
|
524
|
+
return streamFailure;
|
|
525
|
+
}
|
|
526
|
+
const grpcStatus = responseTrailers["grpc-status"] ?? responseHeaders["grpc-status"];
|
|
527
|
+
if (grpcStatus !== undefined && String(grpcStatus) !== "0") {
|
|
528
|
+
streamFailure = cursorRunGrpcError(String(grpcStatus), responseHeaders, responseTrailers);
|
|
529
|
+
return streamFailure;
|
|
530
|
+
}
|
|
531
|
+
if (rawStreamError) {
|
|
532
|
+
streamFailure = toTransportError(rawStreamError, "Cursor Run transport interrupted");
|
|
533
|
+
return streamFailure;
|
|
534
|
+
}
|
|
535
|
+
return undefined;
|
|
536
|
+
};
|
|
537
|
+
const settleTerminal = (event) => {
|
|
538
|
+
if (terminalEvent)
|
|
539
|
+
return;
|
|
540
|
+
terminalEvent = event;
|
|
541
|
+
for (const listener of terminalListeners) {
|
|
542
|
+
try {
|
|
543
|
+
listener(event);
|
|
544
|
+
}
|
|
545
|
+
catch { /* observers cannot destabilize teardown */ }
|
|
546
|
+
}
|
|
547
|
+
terminalListeners.clear();
|
|
548
|
+
};
|
|
549
|
+
const clearReadIdleTimer = () => {
|
|
550
|
+
if (readIdleTimer)
|
|
551
|
+
clearTimeout(readIdleTimer);
|
|
552
|
+
readIdleTimer = undefined;
|
|
553
|
+
};
|
|
554
|
+
const cleanupTerminalResources = () => {
|
|
555
|
+
clearReadIdleTimer();
|
|
556
|
+
if (abortSignal && abortHandler) {
|
|
557
|
+
abortSignal.removeEventListener("abort", abortHandler);
|
|
558
|
+
abortHandler = undefined;
|
|
559
|
+
}
|
|
560
|
+
};
|
|
561
|
+
const finishInbound = (error) => {
|
|
562
|
+
if (error) {
|
|
563
|
+
inboundFailure = error;
|
|
564
|
+
pendingChunks.length = 0;
|
|
565
|
+
}
|
|
566
|
+
else if (!inboundFailure) {
|
|
567
|
+
inboundEnded = true;
|
|
568
|
+
}
|
|
569
|
+
const waiter = inboundWaiter;
|
|
570
|
+
inboundWaiter = undefined;
|
|
571
|
+
if (!waiter)
|
|
572
|
+
return;
|
|
573
|
+
if (inboundFailure)
|
|
574
|
+
waiter.reject(inboundFailure);
|
|
575
|
+
else
|
|
576
|
+
waiter.resolve(undefined);
|
|
577
|
+
};
|
|
578
|
+
const stopInboundReader = () => {
|
|
579
|
+
cleanupTerminalResources();
|
|
580
|
+
if (inboundReaderStarted) {
|
|
581
|
+
stream.removeListener("data", onInboundChunk);
|
|
582
|
+
inboundReaderStarted = false;
|
|
583
|
+
}
|
|
584
|
+
};
|
|
585
|
+
const nextInboundFrame = () => {
|
|
586
|
+
const queued = inboundFrames.shift();
|
|
587
|
+
if (queued)
|
|
588
|
+
return Promise.resolve(queued);
|
|
589
|
+
if (inboundFailure)
|
|
590
|
+
return Promise.reject(inboundFailure);
|
|
591
|
+
if (inboundEnded)
|
|
592
|
+
return Promise.resolve(undefined);
|
|
593
|
+
return new Promise((resolve, reject) => {
|
|
594
|
+
inboundWaiter = { resolve, reject };
|
|
595
|
+
});
|
|
596
|
+
};
|
|
597
|
+
const enqueueFrame = (frame) => {
|
|
598
|
+
const waiter = inboundWaiter;
|
|
599
|
+
inboundWaiter = undefined;
|
|
600
|
+
if (waiter)
|
|
601
|
+
waiter.resolve(frame);
|
|
602
|
+
else
|
|
603
|
+
inboundFrames.push(frame);
|
|
604
|
+
};
|
|
605
|
+
const settleObservedTerminal = () => {
|
|
606
|
+
terminalSettlementScheduled = false;
|
|
607
|
+
if (terminalEvent)
|
|
608
|
+
return;
|
|
609
|
+
if (locallyClosed) {
|
|
610
|
+
finishInbound();
|
|
611
|
+
stopInboundReader();
|
|
612
|
+
settleTerminal({ kind: "local-close" });
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
615
|
+
const failure = observedFailure();
|
|
616
|
+
finishInbound(failure);
|
|
617
|
+
stopInboundReader();
|
|
618
|
+
settleTerminal(failure
|
|
619
|
+
? { kind: "remote-error", error: failure }
|
|
620
|
+
: { kind: "remote-clean-close" });
|
|
621
|
+
};
|
|
622
|
+
const scheduleTerminalSettlement = () => {
|
|
623
|
+
if (terminalEvent || terminalSettlementScheduled)
|
|
624
|
+
return;
|
|
625
|
+
terminalSettlementScheduled = true;
|
|
626
|
+
// HTTP/2 often emits end/aborted immediately before error/close. One turn
|
|
627
|
+
// lets the strongest terminal metadata win.
|
|
628
|
+
setImmediate(settleObservedTerminal);
|
|
629
|
+
};
|
|
630
|
+
const onReadIdleTimeout = () => {
|
|
631
|
+
readIdleTimer = undefined;
|
|
632
|
+
if (locallyClosed || remotelyClosed || streamFailure)
|
|
633
|
+
return;
|
|
634
|
+
streamFailure = new CursorTransportError(`Cursor Run stream read-idle timeout after ${readIdleMs}ms — connection presumed dead`, { transient: true, replaySafe: true, code: "CURSOR_READ_IDLE_TIMEOUT" });
|
|
635
|
+
writable = false;
|
|
636
|
+
finishInbound(streamFailure);
|
|
637
|
+
stopInboundReader();
|
|
638
|
+
settleTerminal({ kind: "remote-error", error: streamFailure });
|
|
639
|
+
try {
|
|
640
|
+
stream.destroy(streamFailure);
|
|
641
|
+
}
|
|
642
|
+
catch { /* already closing */ }
|
|
643
|
+
};
|
|
644
|
+
const armReadIdleWatchdog = () => {
|
|
645
|
+
clearReadIdleTimer();
|
|
646
|
+
if (locallyClosed || remotelyClosed || streamFailure)
|
|
647
|
+
return;
|
|
648
|
+
readIdleTimer = setTimeout(onReadIdleTimeout, readIdleMs);
|
|
649
|
+
readIdleTimer.unref?.();
|
|
650
|
+
};
|
|
651
|
+
const onInboundChunk = (chunk) => {
|
|
652
|
+
armReadIdleWatchdog();
|
|
653
|
+
if (inboundEnded || inboundFailure)
|
|
654
|
+
return;
|
|
655
|
+
pendingChunks.push(new Uint8Array(chunk));
|
|
656
|
+
const merged = mergeBuffers(pendingChunks);
|
|
657
|
+
const parsed = Array.from(streamFrames(merged));
|
|
658
|
+
const consumed = parsed.reduce((sum, frame) => sum + 5 + frame.payload.length, 0);
|
|
659
|
+
pendingChunks.length = 0;
|
|
660
|
+
if (consumed < merged.length)
|
|
661
|
+
pendingChunks.push(merged.subarray(consumed));
|
|
662
|
+
for (const frame of parsed)
|
|
663
|
+
enqueueFrame(frame);
|
|
664
|
+
};
|
|
665
|
+
const startInboundReader = () => {
|
|
666
|
+
if (inboundReaderStarted || locallyClosed || remotelyClosed || streamFailure)
|
|
667
|
+
return;
|
|
668
|
+
inboundReaderStarted = true;
|
|
669
|
+
stream.on("data", onInboundChunk);
|
|
670
|
+
armReadIdleWatchdog();
|
|
671
|
+
};
|
|
232
672
|
// Capture the HTTP/2 response status/headers and any stream-level error.
|
|
233
673
|
// Without this, a non-200 or RST_STREAM surfaces as a silent clean end
|
|
234
674
|
// (frames() just stops) — which looks exactly like "no response, no error".
|
|
@@ -237,78 +677,202 @@ export async function bidiRunStream(token, options) {
|
|
|
237
677
|
responseStatus = h[":status"] !== undefined ? Number(h[":status"]) : 0;
|
|
238
678
|
trace(`h2 response: status=${responseStatus} headers=${JSON.stringify(stripPseudo(h))}`);
|
|
239
679
|
});
|
|
680
|
+
stream.on("trailers", (h) => {
|
|
681
|
+
responseTrailers = h;
|
|
682
|
+
trace(`h2 trailers: ${JSON.stringify(stripPseudo(h))}`);
|
|
683
|
+
});
|
|
240
684
|
stream.on("error", (err) => {
|
|
241
|
-
|
|
685
|
+
rawStreamError = err;
|
|
686
|
+
writable = false;
|
|
242
687
|
trace(`h2 stream error: ${err?.name}: ${err?.message}`);
|
|
688
|
+
scheduleTerminalSettlement();
|
|
243
689
|
});
|
|
244
|
-
stream.on("
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
690
|
+
stream.on("aborted", () => {
|
|
691
|
+
writable = false;
|
|
692
|
+
rawStreamError ??= new CursorTransportError("Cursor Run stream aborted by remote", {
|
|
693
|
+
transient: true,
|
|
694
|
+
replaySafe: true,
|
|
695
|
+
rstCode: stream.rstCode,
|
|
696
|
+
code: "ERR_HTTP2_STREAM_CANCEL",
|
|
697
|
+
});
|
|
698
|
+
scheduleTerminalSettlement();
|
|
699
|
+
});
|
|
700
|
+
stream.on("end", () => {
|
|
701
|
+
writable = false;
|
|
702
|
+
scheduleTerminalSettlement();
|
|
703
|
+
});
|
|
704
|
+
stream.on("close", () => {
|
|
705
|
+
writable = false;
|
|
706
|
+
remotelyClosed = !locallyClosed;
|
|
707
|
+
if (remotelyClosed) {
|
|
708
|
+
dropSession(origin, session);
|
|
709
|
+
// Stop assigning sibling Runs to a connection that remotely lost one
|
|
710
|
+
// of its streams. close() drains existing streams without destroying
|
|
711
|
+
// them; the next Run opens a fresh HTTP/2 session.
|
|
712
|
+
try {
|
|
713
|
+
session.close();
|
|
714
|
+
}
|
|
715
|
+
catch { /* already closed */ }
|
|
716
|
+
}
|
|
717
|
+
trace(`h2 stream closed (status=${responseStatus}, local=${locallyClosed}, ` +
|
|
718
|
+
`err=${rawStreamError instanceof Error ? rawStreamError.message : "none"})`);
|
|
719
|
+
scheduleTerminalSettlement();
|
|
720
|
+
});
|
|
721
|
+
if (abortSignal) {
|
|
722
|
+
abortHandler = () => {
|
|
723
|
+
if (!locallyClosed) {
|
|
724
|
+
locallyClosed = true;
|
|
249
725
|
writable = false;
|
|
726
|
+
inboundFrames.length = 0;
|
|
727
|
+
pendingChunks.length = 0;
|
|
728
|
+
finishInbound();
|
|
729
|
+
stopInboundReader();
|
|
730
|
+
settleTerminal({ kind: "local-close" });
|
|
250
731
|
stream.close();
|
|
251
732
|
}
|
|
252
|
-
}
|
|
733
|
+
};
|
|
734
|
+
abortSignal.addEventListener("abort", abortHandler, { once: true });
|
|
735
|
+
if (abortSignal.aborted)
|
|
736
|
+
abortHandler();
|
|
253
737
|
}
|
|
738
|
+
startInboundReader();
|
|
254
739
|
return {
|
|
255
740
|
write(msg) {
|
|
256
|
-
if (!writable)
|
|
257
|
-
|
|
741
|
+
if (!writable || remotelyClosed || stream.closed || stream.destroyed) {
|
|
742
|
+
if (locallyClosed) {
|
|
743
|
+
throw new CursorLocalCancellationError("Cursor Run stream is closed locally");
|
|
744
|
+
}
|
|
745
|
+
throw observedFailure() ?? new CursorRunInterruptedError("Cursor Run stream is no longer writable");
|
|
746
|
+
}
|
|
258
747
|
const frame = encodeFrame(0x00, msg);
|
|
259
|
-
|
|
748
|
+
try {
|
|
749
|
+
const accepted = stream.write(frame);
|
|
750
|
+
backpressured = !accepted;
|
|
751
|
+
return accepted;
|
|
752
|
+
}
|
|
753
|
+
catch (cause) {
|
|
754
|
+
rawStreamError = cause;
|
|
755
|
+
streamFailure = toTransportError(cause, "Cursor Run stream write failed");
|
|
756
|
+
streamFailure.replaySafe = false;
|
|
757
|
+
writable = false;
|
|
758
|
+
scheduleTerminalSettlement();
|
|
759
|
+
throw streamFailure;
|
|
760
|
+
}
|
|
761
|
+
},
|
|
762
|
+
waitForDrain(timeout) {
|
|
763
|
+
if (!backpressured)
|
|
764
|
+
return Promise.resolve();
|
|
765
|
+
const drainTimeoutMs = timeoutMs("Cursor write drain timeout", timeout, timeout);
|
|
766
|
+
return new Promise((resolve, reject) => {
|
|
767
|
+
let settled = false;
|
|
768
|
+
const finish = (error) => {
|
|
769
|
+
if (settled)
|
|
770
|
+
return;
|
|
771
|
+
settled = true;
|
|
772
|
+
clearTimeout(timer);
|
|
773
|
+
stream.removeListener("drain", onDrain);
|
|
774
|
+
stream.removeListener("error", onError);
|
|
775
|
+
stream.removeListener("close", onClose);
|
|
776
|
+
if (error)
|
|
777
|
+
reject(error);
|
|
778
|
+
else
|
|
779
|
+
resolve();
|
|
780
|
+
};
|
|
781
|
+
const onDrain = () => {
|
|
782
|
+
backpressured = false;
|
|
783
|
+
finish();
|
|
784
|
+
};
|
|
785
|
+
const onError = (cause) => {
|
|
786
|
+
streamFailure ??= toTransportError(cause, "Cursor Run stream drain failed");
|
|
787
|
+
streamFailure.replaySafe = false;
|
|
788
|
+
finish(streamFailure);
|
|
789
|
+
};
|
|
790
|
+
const onClose = () => {
|
|
791
|
+
finish(locallyClosed
|
|
792
|
+
? new CursorLocalCancellationError("Cursor Run stream closed locally during drain")
|
|
793
|
+
: observedFailure() ?? new CursorRunInterruptedError("Cursor Run stream closed before drain"));
|
|
794
|
+
};
|
|
795
|
+
const timer = setTimeout(() => {
|
|
796
|
+
streamFailure ??= new CursorTransportError(`Cursor Run stream backpressure did not drain after ${drainTimeoutMs}ms`, { transient: false, replaySafe: false, code: WRITE_DRAIN_TIMEOUT_CODE });
|
|
797
|
+
finish(streamFailure);
|
|
798
|
+
try {
|
|
799
|
+
stream.destroy(streamFailure);
|
|
800
|
+
}
|
|
801
|
+
catch { /* already closing */ }
|
|
802
|
+
}, drainTimeoutMs);
|
|
803
|
+
timer.unref?.();
|
|
804
|
+
stream.once("drain", onDrain);
|
|
805
|
+
stream.once("error", onError);
|
|
806
|
+
stream.once("close", onClose);
|
|
807
|
+
});
|
|
260
808
|
},
|
|
261
809
|
end() {
|
|
810
|
+
locallyClosed = true;
|
|
262
811
|
writable = false;
|
|
812
|
+
inboundFrames.length = 0;
|
|
813
|
+
pendingChunks.length = 0;
|
|
814
|
+
finishInbound();
|
|
815
|
+
stopInboundReader();
|
|
816
|
+
settleTerminal({ kind: "local-close" });
|
|
263
817
|
stream.end();
|
|
264
818
|
},
|
|
265
819
|
async *frames() {
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
const merged = mergeBuffers(buffer);
|
|
274
|
-
const parsed = Array.from(streamFrames(merged));
|
|
275
|
-
if (parsed.length > 0) {
|
|
276
|
-
const consumed = parsed.reduce((sum, f) => sum + 5 + f.payload.length, 0);
|
|
277
|
-
// Only clear if we fully consumed all pending data
|
|
278
|
-
if (consumed === merged.length) {
|
|
279
|
-
buffer.length = 0;
|
|
280
|
-
}
|
|
281
|
-
else {
|
|
282
|
-
buffer.length = 0;
|
|
283
|
-
buffer.push(merged.subarray(consumed));
|
|
284
|
-
}
|
|
285
|
-
for (const frame of parsed) {
|
|
286
|
-
trace(`frame yield: flags=0x${frame.flags.toString(16)} payload=${frame.payload.length}B`);
|
|
287
|
-
yield frame;
|
|
288
|
-
}
|
|
820
|
+
try {
|
|
821
|
+
while (true) {
|
|
822
|
+
const frame = await nextInboundFrame();
|
|
823
|
+
if (!frame)
|
|
824
|
+
break;
|
|
825
|
+
trace(`frame yield: flags=0x${frame.flags.toString(16)} payload=${frame.payload.length}B`);
|
|
826
|
+
yield frame;
|
|
289
827
|
}
|
|
828
|
+
remotelyClosed = !locallyClosed;
|
|
829
|
+
if (locallyClosed)
|
|
830
|
+
return;
|
|
831
|
+
// A clean HTTP/2 end before Cursor's turn_ended is still an interrupted
|
|
832
|
+
// Run at the provider layer.
|
|
833
|
+
throw observedFailure() ?? cursorRunTerminationError({
|
|
834
|
+
responseStatus,
|
|
835
|
+
responseHeaders,
|
|
836
|
+
responseTrailers,
|
|
837
|
+
});
|
|
290
838
|
}
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
throw
|
|
297
|
-
if (responseStatus !== 0 && responseStatus !== 200) {
|
|
298
|
-
throw new Error(`Cursor Run HTTP ${responseStatus} ${JSON.stringify(stripPseudo(responseHeaders))}`);
|
|
839
|
+
catch (error) {
|
|
840
|
+
if (locallyClosed)
|
|
841
|
+
return;
|
|
842
|
+
if (error instanceof CursorProviderError)
|
|
843
|
+
throw error;
|
|
844
|
+
throw toTransportError(error, `Cursor Run transport interrupted: ${error instanceof Error ? error.message : "unknown error"}`);
|
|
299
845
|
}
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
throw new Error(`Cursor Run gRPC status ${grpcStatus}: ${responseHeaders["grpc-message"] ?? ""}`.trim());
|
|
846
|
+
finally {
|
|
847
|
+
stopInboundReader();
|
|
303
848
|
}
|
|
304
849
|
},
|
|
305
850
|
destroy() {
|
|
306
|
-
if (!
|
|
307
|
-
|
|
851
|
+
if (!locallyClosed) {
|
|
852
|
+
locallyClosed = true;
|
|
308
853
|
writable = false;
|
|
854
|
+
inboundFrames.length = 0;
|
|
855
|
+
pendingChunks.length = 0;
|
|
856
|
+
finishInbound();
|
|
857
|
+
stopInboundReader();
|
|
858
|
+
settleTerminal({ kind: "local-close" });
|
|
309
859
|
stream.close();
|
|
310
860
|
}
|
|
311
861
|
},
|
|
862
|
+
isClosed() {
|
|
863
|
+
return remotelyClosed || locallyClosed || stream.closed || stream.destroyed;
|
|
864
|
+
},
|
|
865
|
+
onTerminal(listener) {
|
|
866
|
+
if (terminalEvent) {
|
|
867
|
+
try {
|
|
868
|
+
listener(terminalEvent);
|
|
869
|
+
}
|
|
870
|
+
catch { /* observers cannot destabilize teardown */ }
|
|
871
|
+
return () => { };
|
|
872
|
+
}
|
|
873
|
+
terminalListeners.add(listener);
|
|
874
|
+
return () => terminalListeners.delete(listener);
|
|
875
|
+
},
|
|
312
876
|
};
|
|
313
877
|
}
|
|
314
878
|
function mergeBuffers(buffers) {
|