@vidit19sharma/opencode-cursor-auth 0.1.0 → 0.3.1

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/dist/bridge.js CHANGED
@@ -8,12 +8,13 @@
8
8
  * next HTTP request (role:"tool" messages) resolves the parked promises and
9
9
  * the SAME run continues — one metered Cursor run per opencode tool loop.
10
10
  */
11
- import { Agent } from "@cursor/sdk";
11
+ import { Agent, AuthenticationError, ConfigurationError, NetworkError, RateLimitError } from "@cursor/sdk";
12
12
  import { createHash, randomUUID } from "node:crypto";
13
13
  import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
14
14
  import { mkdtemp, readFile, writeFile } from "node:fs/promises";
15
15
  import { homedir, tmpdir } from "node:os";
16
16
  import { join } from "node:path";
17
+ import { markBlocked, resolveVariant } from "./models.js";
17
18
  // @cursor/sdk's internal logger writes "HH:MM:SS.mmm INFO ..." lines via
18
19
  // console.log unconditionally (no config/env to silence it, verified against
19
20
  // the esm bundle). Filter exactly those lines, pass everything else through.
@@ -31,6 +32,111 @@ function textOf(content) {
31
32
  return "";
32
33
  return content.map((p) => p.text ?? "").join("");
33
34
  }
35
+ function statusFor(err) {
36
+ const e = (typeof err === "object" && err !== null ? err : {});
37
+ const msg = String(e.message ?? err ?? "");
38
+ const code = typeof e.code === "string" ? e.code : "";
39
+ if (err instanceof AuthenticationError || e.status === 401 || code === "unauthenticated" || /invalid user api key/i.test(msg))
40
+ return 401;
41
+ if (err instanceof RateLimitError || e.status === 429 || code === "resource_exhausted" || /rate limit/i.test(msg))
42
+ return 429;
43
+ if (code === "permission_denied" || /model blocked|permission denied|forbidden/i.test(msg))
44
+ return 403;
45
+ if (err instanceof ConfigurationError || code === "invalid_argument" || code === "not_found" || /cannot use this model|unknown model/i.test(msg))
46
+ return 400;
47
+ return 502;
48
+ }
49
+ const ERROR_TYPE = {
50
+ 400: "invalid_request_error",
51
+ 401: "authentication_error",
52
+ 403: "permission_error",
53
+ 429: "rate_limit_error",
54
+ };
55
+ function errorBody(err, status = statusFor(err)) {
56
+ const e = (typeof err === "object" && err !== null ? err : {});
57
+ return {
58
+ error: {
59
+ message: String(e.message ?? err ?? "unknown error"),
60
+ type: ERROR_TYPE[status] ?? "api_error",
61
+ ...(typeof e.code === "string" ? { code: e.code } : {}),
62
+ },
63
+ };
64
+ }
65
+ const abortError = () => new DOMException("aborted", "AbortError");
66
+ export function isTransient(err) {
67
+ const e = (typeof err === "object" && err !== null ? err : {});
68
+ if (e.name === "AbortError")
69
+ return false;
70
+ const status = statusFor(err);
71
+ if (status === 429)
72
+ return true;
73
+ if (status === 400 || status === 401 || status === 403 || e.status === 404)
74
+ return false;
75
+ if (err instanceof NetworkError || (typeof e.status === "number" && e.status >= 500))
76
+ return true;
77
+ return /unavailable|deadline_exceeded|ECONNRESET|ETIMEDOUT|fetch failed/i.test(`${e.code ?? ""} ${e.message ?? ""}`);
78
+ }
79
+ export async function withRetry(fn, signal, baseDelayMs = 1000, retryable = isTransient) {
80
+ for (let attempt = 0;; attempt++) {
81
+ if (signal?.aborted)
82
+ throw abortError();
83
+ try {
84
+ return await fn();
85
+ }
86
+ catch (err) {
87
+ if (attempt >= 2 || signal?.aborted || !retryable(err))
88
+ throw err;
89
+ const delay = Math.min(baseDelayMs * 2 ** attempt * (0.75 + Math.random() * 0.5), 30_000);
90
+ await new Promise((r) => {
91
+ const t = setTimeout(r, delay);
92
+ signal?.addEventListener("abort", () => (clearTimeout(t), r(undefined)), { once: true });
93
+ });
94
+ }
95
+ }
96
+ }
97
+ // ponytail: global semaphore, slot held across parked tool loops; per-session fairness if it ever matters
98
+ let maxActive = Number(process.env.OPENCODE_CURSOR_MAX_ACTIVE) || 4;
99
+ let active = 0;
100
+ const waiters = [];
101
+ function pump() {
102
+ while (active < maxActive && waiters.length) {
103
+ active++;
104
+ waiters.shift()();
105
+ }
106
+ }
107
+ function acquireSlot(signal) {
108
+ if (signal?.aborted)
109
+ return Promise.reject(abortError());
110
+ if (active < maxActive && !waiters.length) {
111
+ active++;
112
+ return Promise.resolve();
113
+ }
114
+ return new Promise((resolve, reject) => {
115
+ const onAbort = () => {
116
+ const i = waiters.indexOf(go);
117
+ if (i >= 0)
118
+ waiters.splice(i, 1);
119
+ reject(abortError());
120
+ };
121
+ const go = () => {
122
+ signal?.removeEventListener("abort", onAbort);
123
+ resolve();
124
+ };
125
+ signal?.addEventListener("abort", onAbort, { once: true });
126
+ waiters.push(go);
127
+ });
128
+ }
129
+ function releaseSlot() {
130
+ active--;
131
+ pump();
132
+ }
133
+ export function _setMaxActive(n) {
134
+ maxActive = n;
135
+ pump();
136
+ }
137
+ export function errorResponse(err, status = statusFor(err)) {
138
+ return Response.json(errorBody(err, status), { status });
139
+ }
34
140
  function chunk(id, model, delta, finish, usage) {
35
141
  return {
36
142
  id,
@@ -41,7 +147,7 @@ function chunk(id, model, delta, finish, usage) {
41
147
  ...(usage ? { usage } : {}),
42
148
  };
43
149
  }
44
- function sseResponse(model) {
150
+ function sseResponse(model, onCancel) {
45
151
  const id = `chatcmpl-${randomUUID()}`;
46
152
  const enc = new TextEncoder();
47
153
  let controller;
@@ -53,19 +159,31 @@ function sseResponse(model) {
53
159
  },
54
160
  cancel() {
55
161
  closed = true;
162
+ onCancel();
56
163
  },
57
164
  });
165
+ function frame(payload) {
166
+ if (closed)
167
+ return;
168
+ controller.enqueue(enc.encode(`data: ${JSON.stringify(payload)}\n\n`));
169
+ }
58
170
  function send(delta, finish = null, usage) {
171
+ frame(chunk(id, model, delta, finish, usage));
172
+ }
173
+ function close() {
59
174
  if (closed)
60
175
  return;
61
- controller.enqueue(enc.encode(`data: ${JSON.stringify(chunk(id, model, delta, finish, usage))}\n\n`));
176
+ closed = true;
177
+ try {
178
+ controller.close();
179
+ }
180
+ catch { }
62
181
  }
63
182
  function end() {
64
183
  if (closed)
65
184
  return;
66
185
  controller.enqueue(enc.encode("data: [DONE]\n\n"));
67
- controller.close();
68
- closed = true;
186
+ close();
69
187
  }
70
188
  const sink = {
71
189
  text: (t) => send({ content: t }),
@@ -83,10 +201,10 @@ function sseResponse(model) {
83
201
  end();
84
202
  },
85
203
  fail: (err) => {
86
- send({ content: `\n\n[cursor error] ${err instanceof Error ? err.message : String(err)}` });
87
- send({}, "stop");
88
- end();
204
+ frame(errorBody(err));
205
+ close();
89
206
  },
207
+ close,
90
208
  };
91
209
  return {
92
210
  response: new Response(stream, {
@@ -96,13 +214,11 @@ function sseResponse(model) {
96
214
  sink,
97
215
  };
98
216
  }
99
- function jsonCollector(model) {
217
+ function jsonCollector(model, resolve) {
100
218
  const id = `chatcmpl-${randomUUID()}`;
101
219
  let content = "";
102
220
  let calls = [];
103
- let resolve;
104
- const promise = new Promise((r) => (resolve = r));
105
- const sink = {
221
+ return {
106
222
  text: (t) => (content += t),
107
223
  reasoning: () => { },
108
224
  toolCalls: (c) => (calls = calls.concat(c)),
@@ -132,9 +248,18 @@ function jsonCollector(model) {
132
248
  ],
133
249
  usage: usage ?? { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 },
134
250
  })),
135
- fail: (err) => resolve(Response.json({ error: { message: err instanceof Error ? err.message : String(err), type: "cursor_error" } }, { status: 500 })),
251
+ fail: (err) => resolve(errorResponse(err)),
252
+ close: () => { },
136
253
  };
137
- return { promise, sink };
254
+ }
255
+ function deferred() {
256
+ let resolve;
257
+ let reject;
258
+ const promise = new Promise((res, rej) => {
259
+ resolve = res;
260
+ reject = rej;
261
+ });
262
+ return { promise, resolve, reject };
138
263
  }
139
264
  const BATCH_SETTLE_MS = 300;
140
265
  const TOOL_RESULT_TIMEOUT_MS = 10 * 60 * 1000;
@@ -144,13 +269,33 @@ const DISK_SESSION_TTL_MS = 7 * 24 * 60 * 60 * 1000;
144
269
  const MAX_SESSIONS = 8;
145
270
  const SESSION_STORE_DIR = join(homedir(), ".cache", "opencode-cursor-auth");
146
271
  const SESSION_STORE_PATH = join(SESSION_STORE_DIR, "sessions.json");
272
+ const TMP_CWD_PREFIX = join(tmpdir(), "opencode-cursor-");
147
273
  const sessions = new Map();
148
274
  const heldByAgent = new Map();
149
275
  const agentByToolCallId = new Map();
150
276
  let diskSessions = new Map();
151
277
  let sessionsLoaded = false;
152
- function hashMessages(messages) {
278
+ // ponytail: no orphan sweep, other opencode instances may own those dirs
279
+ function releaseCwd(cwd) {
280
+ if (!cwd.startsWith(TMP_CWD_PREFIX))
281
+ return;
282
+ for (const v of sessions.values())
283
+ if (v.cwd === cwd)
284
+ return;
285
+ for (const v of diskSessions.values())
286
+ if (v.cwd === cwd)
287
+ return;
288
+ for (const h of heldByAgent.values())
289
+ if (h.cwd === cwd)
290
+ return;
291
+ rmSync(cwd, { recursive: true, force: true });
292
+ }
293
+ // scope (opencode session id) is folded in so identical histories from
294
+ // different sessions (e.g. parallel sub-agents) never share a Cursor agent
295
+ function hashMessages(messages, scope) {
153
296
  const norm = messages.map((m) => [m.role, textOf(m.content), m.tool_call_id ?? "", JSON.stringify(m.tool_calls ?? null)]);
297
+ if (scope)
298
+ norm.unshift(["scope", scope]);
154
299
  return createHash("sha256").update(JSON.stringify(norm)).digest("hex");
155
300
  }
156
301
  function collectDiskEntries() {
@@ -176,6 +321,9 @@ async function loadSessions() {
176
321
  if (now - v.lastUsedAt <= DISK_SESSION_TTL_MS)
177
322
  diskSessions.set(k, v);
178
323
  }
324
+ for (const v of Object.values(raw.entries))
325
+ if (now - v.lastUsedAt > DISK_SESSION_TTL_MS)
326
+ releaseCwd(v.cwd);
179
327
  }
180
328
  catch { }
181
329
  }
@@ -185,9 +333,17 @@ function saveSessions() {
185
333
  }
186
334
  function sweepSessions(save = true) {
187
335
  const now = Date.now();
336
+ const evicted = [];
337
+ for (const [k, v] of diskSessions)
338
+ if (now - v.lastUsedAt > DISK_SESSION_TTL_MS) {
339
+ diskSessions.delete(k);
340
+ evicted.push(v.cwd);
341
+ }
188
342
  for (const [k, v] of sessions)
189
- if (now - v.lastUsedAt > SESSION_TTL_MS)
343
+ if (now - v.lastUsedAt > SESSION_TTL_MS) {
190
344
  sessions.delete(k);
345
+ evicted.push(v.cwd);
346
+ }
191
347
  while (sessions.size > MAX_SESSIONS) {
192
348
  let oldestKey = "";
193
349
  let oldest = Infinity;
@@ -196,8 +352,11 @@ function sweepSessions(save = true) {
196
352
  oldest = v.lastUsedAt;
197
353
  oldestKey = k;
198
354
  }
355
+ evicted.push(sessions.get(oldestKey).cwd);
199
356
  sessions.delete(oldestKey);
200
357
  }
358
+ for (const cwd of evicted)
359
+ releaseCwd(cwd);
201
360
  if (save)
202
361
  saveSessions();
203
362
  }
@@ -214,15 +373,18 @@ function rememberSession(held, toolCalls) {
214
373
  ? { tool_calls: toolCalls.map((c) => ({ id: c.id, function: { name: c.name, arguments: c.argumentsJson } })) }
215
374
  : {}),
216
375
  };
217
- const key = hashMessages(held.messages.concat(assistant));
376
+ const key = hashMessages(held.messages.concat(assistant), held.scope);
218
377
  const entry = {
219
378
  agentId: held.agent.agentId,
220
379
  cwd: held.cwd,
221
380
  lastUsedAt: Date.now(),
222
381
  };
382
+ const displaced = sessions.get(key) ?? diskSessions.get(key);
223
383
  sessions.set(key, entry);
224
384
  diskSessions.set(key, entry);
225
385
  sweepSessions();
386
+ if (displaced && displaced.cwd !== entry.cwd)
387
+ releaseCwd(displaced.cwd);
226
388
  }
227
389
  async function syncAgentsMd(cwd, messages) {
228
390
  mkdirSync(cwd, { recursive: true });
@@ -242,7 +404,12 @@ function mapUsage(u) {
242
404
  return undefined;
243
405
  const prompt = u.inputTokens ?? 0;
244
406
  const completion = u.outputTokens ?? 0;
245
- return { prompt_tokens: prompt, completion_tokens: completion, total_tokens: prompt + completion };
407
+ const usage = { prompt_tokens: prompt, completion_tokens: completion, total_tokens: prompt + completion };
408
+ if (u.cacheReadTokens)
409
+ usage.prompt_tokens_details = { cached_tokens: u.cacheReadTokens };
410
+ if (u.reasoningTokens)
411
+ usage.completion_tokens_details = { reasoning_tokens: u.reasoningTokens };
412
+ return usage;
246
413
  }
247
414
  // deltas arriving between tool flush and the continuation request get buffered
248
415
  function emit(held, kind, t) {
@@ -251,73 +418,124 @@ function emit(held, kind, t) {
251
418
  else
252
419
  held.buffer.push({ kind, t });
253
420
  }
421
+ function openReply(held) {
422
+ if (held.opened)
423
+ return;
424
+ held.opened = true;
425
+ if (held.response)
426
+ held.reply.resolve(held.response);
427
+ }
254
428
  function attachSink(held, sink) {
255
429
  held.sink = sink;
430
+ if (held.buffer.length)
431
+ openReply(held);
256
432
  for (const b of held.buffer)
257
433
  (b.kind === "text" ? sink.text : sink.reasoning)(b.t);
258
434
  held.buffer = [];
259
435
  }
260
- function teardownHeld(agentId, held) {
436
+ function attachSignal(held, signal) {
437
+ held.detachSignal?.();
438
+ held.detachSignal = null;
439
+ if (!signal)
440
+ return;
441
+ const onAbort = () => abortHeld(held);
442
+ signal.addEventListener("abort", onAbort, { once: true });
443
+ held.detachSignal = () => signal.removeEventListener("abort", onAbort);
444
+ }
445
+ function closeAgent(held) {
446
+ try {
447
+ held.agent.close();
448
+ }
449
+ catch { }
450
+ }
451
+ function teardownHeld(held) {
261
452
  held.done = true;
453
+ if (held.slot) {
454
+ held.slot = false;
455
+ releaseSlot();
456
+ }
457
+ held.detachSignal?.();
458
+ held.detachSignal = null;
262
459
  if (held.flushTimer)
263
460
  clearTimeout(held.flushTimer);
264
461
  if (held.idleTimer)
265
462
  clearTimeout(held.idleTimer);
266
463
  for (const id of held.parked.keys())
267
464
  agentByToolCallId.delete(id);
268
- heldByAgent.delete(agentId);
465
+ heldByAgent.delete(held.agent.agentId);
466
+ }
467
+ function failHeld(held, err) {
468
+ teardownHeld(held);
469
+ if (statusFor(err) === 403)
470
+ markBlocked(held.model);
471
+ if (held.response && !held.opened) {
472
+ held.opened = true;
473
+ held.reply.resolve(errorResponse(err));
474
+ held.sink?.close();
475
+ }
476
+ else {
477
+ held.sink?.fail(err);
478
+ }
479
+ closeAgent(held);
480
+ releaseCwd(held.cwd);
481
+ }
482
+ function abortHeld(held) {
483
+ if (held.done)
484
+ return;
485
+ teardownHeld(held);
486
+ held.run?.cancel().catch(() => { });
487
+ closeAgent(held);
488
+ held.sink?.close();
489
+ held.reply.reject(new DOMException("aborted", "AbortError"));
490
+ releaseCwd(held.cwd);
269
491
  }
270
492
  /** Model requested tools: emit tool_calls, end this HTTP segment, park the run. */
271
- function flushToolCalls(agentId, held) {
493
+ function flushToolCalls(held) {
272
494
  held.flushTimer = null;
273
495
  if (held.done || !held.pendingCalls.length)
274
496
  return;
275
497
  const calls = held.pendingCalls;
276
498
  held.pendingCalls = [];
277
499
  for (const c of calls)
278
- agentByToolCallId.set(c.id, agentId);
500
+ agentByToolCallId.set(c.id, held.agent.agentId);
279
501
  const sink = held.sink;
280
502
  held.sink = null;
503
+ // the parked run outlives this HTTP segment, so its abort signal must not
504
+ // reach it; the continuation request attaches its own
505
+ held.detachSignal?.();
506
+ held.detachSignal = null;
507
+ openReply(held);
281
508
  sink?.toolCalls(calls);
282
509
  sink?.finish("tool_calls");
283
510
  rememberSession(held, calls);
284
- held.idleTimer = setTimeout(() => {
285
- teardownHeld(agentId, held);
286
- held.run.cancel().catch(() => { });
287
- try {
288
- held.agent.close();
289
- }
290
- catch { }
291
- }, TOOL_RESULT_TIMEOUT_MS);
511
+ held.idleTimer = setTimeout(() => abortHeld(held), TOOL_RESULT_TIMEOUT_MS);
292
512
  held.idleTimer.unref?.();
293
513
  }
294
- async function wireRunCompletion(agentId, held) {
295
- if (held.wired)
514
+ async function wireRunCompletion(held) {
515
+ if (held.wired || !held.run)
296
516
  return;
297
517
  held.wired = true;
518
+ let result;
298
519
  try {
299
- const result = await held.run.wait();
300
- if (held.done)
301
- return;
302
- const usage = mapUsage(result.usage) ?? held.usage;
303
- teardownHeld(agentId, held);
304
- rememberSession(held, []);
305
- held.sink?.finish("stop", usage);
306
- try {
307
- held.agent.close();
308
- }
309
- catch { }
520
+ result = await held.run.wait();
310
521
  }
311
522
  catch (err) {
312
- if (held.done)
313
- return;
314
- teardownHeld(agentId, held);
315
- held.sink?.fail(err);
316
- try {
317
- held.agent.close();
318
- }
319
- catch { }
523
+ if (!held.done)
524
+ failHeld(held, err);
525
+ return;
320
526
  }
527
+ if (held.done)
528
+ return;
529
+ if (result.status === "cancelled")
530
+ return abortHeld(held);
531
+ if (result.status === "error")
532
+ return failHeld(held, result.error ?? new Error("cursor run failed"));
533
+ const usage = mapUsage(result.usage) ?? held.usage;
534
+ teardownHeld(held);
535
+ rememberSession(held, []);
536
+ openReply(held);
537
+ held.sink?.finish("stop", usage);
538
+ closeAgent(held);
321
539
  }
322
540
  export function _resetForTest() {
323
541
  sessions.clear();
@@ -326,79 +544,110 @@ export function _resetForTest() {
326
544
  agentByToolCallId.clear();
327
545
  sessionsLoaded = false;
328
546
  }
329
- export async function handleChatCompletion(body, apiKey) {
547
+ export function _stats() {
548
+ return {
549
+ held: heldByAgent.size,
550
+ sessions: sessions.size,
551
+ disk: diskSessions.size,
552
+ agentIds: [...sessions.values()].map((v) => v.agentId).sort(),
553
+ active,
554
+ waiting: waiters.length,
555
+ };
556
+ }
557
+ export async function handleChatCompletion(body, apiKey, signal, scope) {
558
+ if (signal?.aborted)
559
+ throw new DOMException("aborted", "AbortError");
330
560
  await loadSessions();
331
561
  const streaming = body.stream !== false;
332
- const sse = streaming ? sseResponse(body.model) : undefined;
333
- const collected = streaming ? undefined : jsonCollector(body.model);
334
- const sink = (sse?.sink ?? collected?.sink);
335
- const reply = () => sse?.response ?? collected.promise;
562
+ const reply = deferred();
563
+ let held;
564
+ const sse = streaming ? sseResponse(body.model, () => held && abortHeld(held)) : undefined;
565
+ const sink = sse?.sink ?? jsonCollector(body.model, reply.resolve);
336
566
  // continuation: trailing tool results for a parked run
337
567
  const lastMsg = body.messages[body.messages.length - 1];
338
- if (lastMsg?.role === "tool" && lastMsg.tool_call_id && agentByToolCallId.has(lastMsg.tool_call_id)) {
568
+ if (lastMsg?.role === "tool" && lastMsg.tool_call_id) {
339
569
  const agentId = agentByToolCallId.get(lastMsg.tool_call_id);
340
- const held = heldByAgent.get(agentId);
341
- if (held && !held.done) {
342
- if (held.idleTimer)
343
- clearTimeout(held.idleTimer);
344
- held.messages = body.messages;
345
- held.textAcc = "";
346
- attachSink(held, sink);
347
- for (const m of body.messages) {
348
- if (m.role !== "tool" || !m.tool_call_id)
349
- continue;
350
- const resolve = held.parked.get(m.tool_call_id);
351
- if (resolve) {
352
- held.parked.delete(m.tool_call_id);
353
- agentByToolCallId.delete(m.tool_call_id);
354
- resolve(textOf(m.content));
355
- }
570
+ held = agentId ? heldByAgent.get(agentId) : undefined;
571
+ if (!held || held.done) {
572
+ return Response.json({ error: { message: "cursor run for this tool call expired — please retry", type: "invalid_request_error", code: "tool_call_expired" } }, { status: 400 });
573
+ }
574
+ if (held.idleTimer)
575
+ clearTimeout(held.idleTimer);
576
+ held.messages = body.messages;
577
+ held.textAcc = "";
578
+ held.reply = reply;
579
+ held.response = sse?.response ?? null;
580
+ held.opened = false;
581
+ attachSignal(held, signal);
582
+ attachSink(held, sink);
583
+ for (const m of body.messages) {
584
+ if (m.role !== "tool" || !m.tool_call_id)
585
+ continue;
586
+ const resolve = held.parked.get(m.tool_call_id);
587
+ if (resolve) {
588
+ held.parked.delete(m.tool_call_id);
589
+ agentByToolCallId.delete(m.tool_call_id);
590
+ resolve(textOf(m.content));
356
591
  }
357
- return reply();
358
592
  }
359
- sink.fail(new Error("cursor run for this tool call expired — please retry"));
360
- return reply();
593
+ return reply.promise;
361
594
  }
595
+ // opencode wraps variant options as providerOptions.cursor; @ai-sdk/openai-compatible (name "cursor") spreads them into the body top level
596
+ const model = resolveVariant(body.model, body.cursor_params) ?? { id: body.model };
597
+ if (typeof model === "string")
598
+ return errorResponse(model, 400);
362
599
  // new turn: reuse the agent whose last-seen conversation matches our prefix
363
600
  const prefix = body.messages.slice(0, -1);
364
- const key = hashMessages(prefix);
601
+ const key = hashMessages(prefix, scope);
602
+ await acquireSlot(signal);
365
603
  const entry = sessions.get(key) ?? diskSessions.get(key);
366
604
  let agent;
367
605
  let cwd;
368
606
  let newSuffix = body.messages;
607
+ const agentOptions = (cwd) => ({
608
+ model,
609
+ tools: ["mcp"],
610
+ ...(apiKey ? { apiKey } : {}),
611
+ local: { cwd, settingSources: ["project"], enableAgentRetries: true },
612
+ });
369
613
  if (entry) {
370
614
  sessions.delete(key);
615
+ diskSessions.delete(key);
371
616
  cwd = entry.cwd;
372
- agent = await Agent.resume(entry.agentId, {
373
- model: { id: body.model },
374
- tools: ["mcp"],
375
- ...(apiKey ? { apiKey } : {}),
376
- local: { cwd, settingSources: ["project"], enableAgentRetries: true },
377
- }).catch(() => {
378
- sessions.delete(key);
379
- diskSessions.delete(key);
617
+ agent = await withRetry(() => Agent.resume(entry.agentId, agentOptions(cwd)), signal).catch(() => {
380
618
  saveSessions();
619
+ releaseCwd(entry.cwd);
381
620
  return undefined;
382
621
  });
383
622
  if (agent)
384
623
  newSuffix = body.messages.slice(-1);
385
624
  }
386
625
  if (!agent) {
387
- cwd = await mkdtemp(join(tmpdir(), "opencode-cursor-"));
388
- agent = await Agent.create({
389
- model: { id: body.model },
390
- tools: ["mcp"],
391
- ...(apiKey ? { apiKey } : {}),
392
- local: { cwd, settingSources: ["project"], enableAgentRetries: true },
393
- });
626
+ cwd = await mkdtemp(TMP_CWD_PREFIX);
627
+ try {
628
+ agent = await withRetry(() => Agent.create(agentOptions(cwd)), signal);
629
+ }
630
+ catch (err) {
631
+ releaseSlot();
632
+ releaseCwd(cwd);
633
+ if (signal?.aborted)
634
+ throw abortError();
635
+ return errorResponse(err);
636
+ }
394
637
  }
395
- const held = {
638
+ held = {
396
639
  agent,
397
640
  cwd: cwd,
398
- run: undefined,
641
+ model: body.model,
642
+ run: null,
399
643
  messages: body.messages,
644
+ scope,
400
645
  textAcc: "",
401
646
  sink: null,
647
+ reply,
648
+ response: sse?.response ?? null,
649
+ opened: false,
650
+ detachSignal: null,
402
651
  buffer: [],
403
652
  pendingCalls: [],
404
653
  parked: new Map(),
@@ -406,9 +655,12 @@ export async function handleChatCompletion(body, apiKey) {
406
655
  idleTimer: null,
407
656
  wired: false,
408
657
  done: false,
658
+ slot: true,
409
659
  };
660
+ const self = held;
661
+ heldByAgent.set(agent.agentId, held);
662
+ attachSignal(held, signal);
410
663
  attachSink(held, sink);
411
- const agentIdRef = { id: "" };
412
664
  const customTools = {};
413
665
  for (const t of body.tools ?? []) {
414
666
  customTools[t.function.name] = {
@@ -416,11 +668,11 @@ export async function handleChatCompletion(body, apiKey) {
416
668
  inputSchema: t.function.parameters ?? { type: "object", properties: {} },
417
669
  execute: (args, ctx) => {
418
670
  const id = (ctx.toolCallId ?? `call_${randomUUID()}`).replace(/\s+/g, "_");
419
- held.pendingCalls.push({ id, name: t.function.name, argumentsJson: JSON.stringify(args ?? {}) });
420
- if (held.flushTimer)
421
- clearTimeout(held.flushTimer);
422
- held.flushTimer = setTimeout(() => flushToolCalls(agentIdRef.id, held), BATCH_SETTLE_MS);
423
- return new Promise((resolve) => held.parked.set(id, resolve));
671
+ self.pendingCalls.push({ id, name: t.function.name, argumentsJson: JSON.stringify(args ?? {}) });
672
+ if (self.flushTimer)
673
+ clearTimeout(self.flushTimer);
674
+ self.flushTimer = setTimeout(() => flushToolCalls(self), BATCH_SETTLE_MS);
675
+ return new Promise((resolve) => self.parked.set(id, resolve));
424
676
  },
425
677
  };
426
678
  }
@@ -430,29 +682,40 @@ export async function handleChatCompletion(body, apiKey) {
430
682
  .map((m) => (m.role === "user" ? textOf(m.content) : `${m.role.toUpperCase()}: ${textOf(m.content)}`))
431
683
  .filter(Boolean)
432
684
  .join("\n\n") || "(empty message)";
685
+ let run;
433
686
  try {
434
- held.run = await held.agent.send(prompt, {
687
+ run = await withRetry(() => agent.send(prompt, {
435
688
  onDelta: ({ update }) => {
689
+ if (self.done)
690
+ return;
436
691
  if (update.type === "text-delta" && update.text) {
437
- held.textAcc += update.text;
438
- emit(held, "text", update.text);
692
+ openReply(self);
693
+ self.textAcc += update.text;
694
+ emit(self, "text", update.text);
439
695
  }
440
696
  else if (update.type === "thinking-delta" && update.text) {
441
- emit(held, "reasoning", update.text);
697
+ openReply(self);
698
+ emit(self, "reasoning", update.text);
442
699
  }
443
700
  else if (update.type === "turn-ended") {
444
- held.usage = mapUsage(update.usage);
701
+ self.usage = mapUsage(update.usage);
445
702
  }
446
703
  },
447
704
  local: { customTools: customTools },
448
- });
705
+ }), signal, 1000, (err) => !self.opened && !self.done && isTransient(err));
449
706
  }
450
707
  catch (err) {
451
- sink.fail(err);
452
- return reply();
708
+ if (!self.done)
709
+ failHeld(self, err);
710
+ return reply.promise;
711
+ }
712
+ if (self.done) {
713
+ run.cancel().catch(() => { });
714
+ return reply.promise;
453
715
  }
454
- agentIdRef.id = held.run.agentId;
455
- heldByAgent.set(held.run.agentId, held);
456
- void wireRunCompletion(held.run.agentId, held);
457
- return reply();
716
+ if (run.agentId !== agent.agentId)
717
+ console.error(`[opencode-cursor-auth] run.agentId ${run.agentId} != agent.agentId ${agent.agentId}`);
718
+ self.run = run;
719
+ void wireRunCompletion(self);
720
+ return reply.promise;
458
721
  }