@magpie-community/opencode-cursor-auth 0.2.13 → 0.2.14

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/index.mjs CHANGED
@@ -21,13 +21,15 @@
21
21
  // a conversation, the caller's system prompt at its head; the Run only
22
22
  // resumes it. The caller's tools are MCP tools, which the model calls
23
23
  // through Cursor's CallDynamicTool: the server hands each call to the
24
- // client to run, and once it has said how many a step made, the stream is
25
- // closed and the calls go back to OpenCode as a chat completion's tool
26
- // calls. Nothing else the server asks the client to run (a shell, a file
27
- // read) is run.
24
+ // client to run, and once it has said how many a step made, the calls
25
+ // go back to OpenCode. When the owner is available their results continue
26
+ // that Run; otherwise the caller's history starts a stateless Run. Nothing
27
+ // else the server asks the client to run (a shell, a file read) is run.
28
28
  //
29
29
  // OpenCode speaks chat completions to it (@ai-sdk/openai-compatible); the
30
30
  // fetch here answers them.
31
+ import { NATIVE_LIMITS, nativeSessions, NativeSessionError, nativeFields, nativeMcpArgs } from "./native-session.mjs"
32
+ import { isDeepStrictEqual } from "node:util"
31
33
  import http2 from "node:http2"
32
34
  import { STATUS_CODES } from "node:http"
33
35
  import { gunzipSync } from "node:zlib"
@@ -1396,14 +1398,43 @@ function addUsage(a, b) {
1396
1398
  return a
1397
1399
  }
1398
1400
 
1399
- // owe adds to the account/conversation's debt.
1400
- function owe(key, { since = Date.now(), n = 0, ready = Date.now(), carry = null }) {
1401
- for (const [k, d] of owed) if (Date.now() - d.since > STEP_WAIT.owedFor) owed.delete(k)
1401
+ // owe adds to the account/conversation's debt. A native Run bounds older
1402
+ // event timestamps before its start: its authoritative final must never be
1403
+ // collected as an earlier Run. Keep that bound when a later stateless Run
1404
+ // adds unbounded debt. Ordinary stateless debt retains its original shape.
1405
+ const DEBT_GROUP_LIMIT = 64
1406
+ function owe(key, { since = Date.now(), n = 0, ready = Date.now(), carry = null, before }) {
1407
+ for (const [k, d] of owed) {
1408
+ if (d.groups) {
1409
+ d.groups = d.groups.filter((g) => Date.now() - g.since <= STEP_WAIT.owedFor)
1410
+ d.n = d.groups.reduce((sum, g) => sum + g.n, 0)
1411
+ if (Date.now() - d.since > STEP_WAIT.owedFor) d.carry = null
1412
+ if (!d.n && !d.carry) owed.delete(k)
1413
+ else if (d.groups.length) d.since = Math.min(...d.groups.map((g) => g.since))
1414
+ } else if (Date.now() - d.since > STEP_WAIT.owedFor) owed.delete(k)
1415
+ }
1402
1416
  if (n <= 0 && !carry) return
1403
1417
  const d = owed.get(key)
1404
- if (!d) return void owed.set(key, { since, n, ready, carry: carry && { ...carry } })
1405
- d.since = Math.min(d.since, since)
1406
- d.n += n
1418
+ if (!d) return void owed.set(key, { since, n, ready, carry: carry && { ...carry }, ...(before !== undefined ? { before } : {}) })
1419
+ if (n > 0 && (d.groups || d.before !== undefined || before !== undefined)) {
1420
+ const groups = d.groups ?? (d.n > 0 ? [{ since: d.since, n: d.n, ready: d.ready, before: d.before }] : [])
1421
+ const same = groups.find((g) => g.before === before)
1422
+ if (same) {
1423
+ same.since = Math.min(same.since, since)
1424
+ same.n += n
1425
+ same.ready = Math.min(same.ready, ready)
1426
+ } else groups.push({ since, n, ready, before })
1427
+ // As with expired dashboard debt, overflow stays unknown, never estimated
1428
+ // or merged across bounds into an authoritative native Run's event.
1429
+ groups.sort((a, b) => a.since - b.since)
1430
+ d.groups = groups.slice(-DEBT_GROUP_LIMIT)
1431
+ delete d.before
1432
+ d.n = d.groups.reduce((sum, g) => sum + g.n, 0)
1433
+ d.since = Math.min(...d.groups.map((g) => g.since))
1434
+ } else {
1435
+ d.since = Math.min(d.since, since)
1436
+ d.n += n
1437
+ }
1407
1438
  d.ready = Math.min(d.ready, ready)
1408
1439
  d.carry = addUsage(d.carry, carry)
1409
1440
  }
@@ -1414,7 +1445,7 @@ function owe(key, { since = Date.now(), n = 0, ready = Date.now(), carry = null
1414
1445
  // when the account can't read them.
1415
1446
  // Polling, they are looked for as STEP_WAIT says, from `first` on, until
1416
1447
  // all show; else asked for once.
1417
- async function usageEvents(tok, conv, since, want, { poll = false, first = STEP_WAIT.first, signal } = {}) {
1448
+ async function usageEvents(tok, conv, since, want, { poll = false, first = STEP_WAIT.first, signal, before = () => Infinity, groups, abortFetch = false } = {}) {
1418
1449
  const key = usageKey(tok, conv)
1419
1450
  const end = Date.now() + STEP_WAIT.until
1420
1451
  const wait = (ms) => new Promise((r) => {
@@ -1426,6 +1457,7 @@ async function usageEvents(tok, conv, since, want, { poll = false, first = STEP_
1426
1457
  let usage = null
1427
1458
  let got = 0
1428
1459
  const events = []
1460
+ const counts = groups?.map(() => 0)
1429
1461
  if (poll) await wait(first)
1430
1462
  while (!signal?.aborted) {
1431
1463
  try {
@@ -1433,19 +1465,26 @@ async function usageEvents(tok, conv, since, want, { poll = false, first = STEP_
1433
1465
  method: "POST",
1434
1466
  headers: await headersFor(tok),
1435
1467
  body: JSON.stringify({ teamId: 0, startDate: String(since - 60_000), endDate: String(Date.now() + 60_000), page: 1, pageSize: 50 }),
1436
- signal: AbortSignal.timeout(STEP_WAIT.ask),
1468
+ signal: abortFetch && signal ? AbortSignal.any([signal, AbortSignal.timeout(STEP_WAIT.ask)]) : AbortSignal.timeout(STEP_WAIT.ask),
1437
1469
  })
1438
1470
  if (res.status >= 400 && res.status < 500 && res.status !== 429) {
1439
1471
  // not this account's to read
1440
1472
  unreadable.set(tokKey(tok), Date.now())
1441
- return { usage, events, got, refused: true }
1473
+ return { usage, events, got, counts, refused: true }
1442
1474
  }
1443
1475
  const evs = res.ok ? ((await res.json())?.usageEventsDisplay ?? []) : []
1444
1476
  const found = evs
1445
1477
  .filter((e) => e?.conversationId === conv && e.tokenUsage && num(e.timestamp) >= since - 1000 && !claimed.has(key + "\0" + e.timestamp))
1446
1478
  .sort((a, b) => num(a.timestamp) - num(b.timestamp))
1447
- .slice(0, want - got)
1448
1479
  for (const ev of found) {
1480
+ if (got >= want) break
1481
+ const at = num(ev.timestamp)
1482
+ // found already excludes timestamps claimed by earlier fetches. Two
1483
+ // distinct events in this response may share a millisecond; count both.
1484
+ if (at >= before()) continue
1485
+ const group = groups?.findIndex((g, i) => counts[i] < g.n && at >= g.since - 1000 && at < (g.before ?? Infinity))
1486
+ if (groups && group === -1) continue
1487
+ if (groups) counts[group]++
1449
1488
  claimed.set(key + "\0" + ev.timestamp, Date.now())
1450
1489
  const t = ev.tokenUsage
1451
1490
  const u = { input: num(t.inputTokens), output: num(t.outputTokens), cacheRead: num(t.cacheReadTokens), cacheWrite: num(t.cacheWriteTokens), reasoning: 0 }
@@ -1458,7 +1497,7 @@ async function usageEvents(tok, conv, since, want, { poll = false, first = STEP_
1458
1497
  if (!poll || Date.now() + STEP_WAIT.every > end) break
1459
1498
  await wait(STEP_WAIT.every)
1460
1499
  }
1461
- return { usage, events, got, refused: false }
1500
+ return { usage, events, got, counts, refused: false }
1462
1501
  }
1463
1502
 
1464
1503
  // stepUsage is the usage of the Run of conversation conv started at since,
@@ -1472,22 +1511,35 @@ async function stepUsage(tok, conv, since, signal) {
1472
1511
  // what was carried alone (carry) and the events found (events), and whether any is
1473
1512
  // still owed (short), which is owed on. What comes back once the Run that
1474
1513
  // asked is over (over() true) is carried to the next instead.
1475
- function collect(tok, conv, { poll = false, first, signal, over = () => false } = {}) {
1514
+ function collect(tok, conv, { poll = false, first, signal, over = () => false, abortFetch = false } = {}) {
1476
1515
  const key = usageKey(tok, conv)
1477
1516
  const d = owed.get(key)
1478
1517
  if (!d) return Promise.resolve({ usage: null, carry: null, events: [], short: false })
1479
1518
  owed.delete(key)
1480
1519
  const p = (async () => {
1520
+ const groups = (d.groups ?? [d]).filter((g) => (g.before === undefined && !d.groups) || Date.now() - g.since <= STEP_WAIT.owedFor)
1521
+ const want = groups.reduce((sum, g) => sum + g.n, 0)
1481
1522
  let r = { usage: null, events: [], got: 0 }
1482
- if (d.n > 0 && canRead(tok)) r = await usageEvents(tok, conv, d.since, d.n, { poll, first, signal }).catch(() => r)
1483
- const short = d.n > r.got && canRead(tok)
1484
- if (short) owe(key, { since: d.since, n: d.n - r.got, ready: d.ready })
1523
+ if (want > 0 && canRead(tok)) r = await usageEvents(tok, conv, Math.min(...groups.map((g) => g.since)), want, {
1524
+ poll, first, signal, abortFetch, before: () => d.before ?? Infinity, ...(d.groups ? { groups } : {}),
1525
+ }).catch(() => r)
1526
+ let short = false
1527
+ for (const [i, g] of groups.entries()) {
1528
+ const remaining = g.n - (d.groups ? r.counts?.[i] ?? 0 : r.got)
1529
+ if (remaining > 0 && canRead(tok)) {
1530
+ short = true
1531
+ owe(key, { since: g.since, n: remaining, ready: g.ready, before: g.before })
1532
+ }
1533
+ }
1485
1534
  const carry = d.carry && { ...d.carry }
1486
1535
  const usage = addUsage(d.carry, r.usage)
1487
1536
  if (!over()) return { usage, carry, events: r.events ?? [], short }
1488
1537
  owe(key, { carry: usage })
1489
1538
  return { usage: null, carry: null, events: [], short }
1490
1539
  })()
1540
+ // Native admission may happen while an older collector is still in flight.
1541
+ // Its dynamic event bound must apply before that response is claimed too.
1542
+ p.debt = d
1491
1543
  const set = flights.get(key) ?? new Set()
1492
1544
  flights.set(key, set.add(p))
1493
1545
  p.finally(() => {
@@ -1517,14 +1569,18 @@ function collect(tok, conv, { poll = false, first, signal, over = () => false }
1517
1569
  // owed(), apart. A Run of its conversation begins only once the one
1518
1570
  // before has ended, so the two never meet; were the time off, the step's
1519
1571
  // count would be told in the wrong field, never twice nor lost.
1520
- function ledger(tok, conv, since, signal, kept) {
1572
+ function ledger(tok, conv, since, signal, kept, { before } = {}) {
1521
1573
  const key = tokKey(tok)
1522
1574
  const debtKey = usageKey(tok, conv)
1575
+ if (kept && before !== undefined) {
1576
+ const debts = [owed.get(debtKey), ...[...(flights.get(debtKey) ?? [])].map((p) => p.debt)]
1577
+ for (const d of debts) for (const g of d?.groups ?? (d ? [d] : [])) g.before = Math.min(g.before ?? Infinity, before)
1578
+ }
1523
1579
  let mine = null // this Run's own, collected, not given out yet
1524
1580
  let theirs = null // earlier Runs', collected, not given out yet
1525
1581
  let over = false
1526
1582
  const gather = (opts) =>
1527
- collect(tok, conv, { ...opts, over: () => over }).then((r) => {
1583
+ collect(tok, conv, { ...opts, over: () => over, abortFetch: before !== undefined }).then((r) => {
1528
1584
  theirs = addUsage(theirs, r.carry)
1529
1585
  for (const e of r.events ?? []) {
1530
1586
  if (e.at >= since - 1000) mine = addUsage(mine, e.usage)
@@ -1876,6 +1932,206 @@ function openH1(headers, signal) {
1876
1932
  return Promise.resolve(signal?.aborted ? cancelledOpen() : { session: { close, destroy: close }, req: Object.assign(req, { close }), status: 200, h1: true })
1877
1933
  }
1878
1934
 
1935
+ // Every outbound frame, including KV replies and heartbeats, uses the same
1936
+ // bounded FIFO. Admission is synchronous; a result is reserved only once even
1937
+ // when a later write fails. Count bytes until both callback and drain complete.
1938
+ function nativeTransport(req, close, fail, limits = NATIVE_LIMITS) {
1939
+ const outbound = { bytes: 0, messages: 0 }
1940
+ const queue = []
1941
+ let closed = false, flushing = false, rejectWrite
1942
+ const flush = async () => {
1943
+ if (closed || flushing) return
1944
+ flushing = true
1945
+ try {
1946
+ while (!closed && queue.length) {
1947
+ const packet = queue.shift()
1948
+ await new Promise((resolve, reject) => {
1949
+ let written = false, drained = false, started = false, settled = false
1950
+ const finish = (error) => {
1951
+ if (settled) return
1952
+ settled = true
1953
+ rejectWrite = undefined
1954
+ req.removeListener("drain", drain)
1955
+ req.removeListener("error", failed)
1956
+ req.removeListener("close", lost)
1957
+ error ? reject(error) : resolve()
1958
+ }
1959
+ const check = () => { if (started && written && drained) finish() }
1960
+ const drain = () => { drained = true; check() }
1961
+ const failed = (e) => finish(e)
1962
+ const lost = () => finish(new NativeSessionError(502, "native transport closed during a write; its outcome is uncertain"))
1963
+ rejectWrite = finish
1964
+ req.once("drain", drain)
1965
+ req.once("error", failed)
1966
+ req.once("close", lost)
1967
+ try {
1968
+ drained = req.write(packet, (e) => {
1969
+ if (e) return finish(e)
1970
+ written = true
1971
+ check()
1972
+ }) || drained
1973
+ started = true
1974
+ if (drained) req.removeListener("drain", drain)
1975
+ check()
1976
+ } catch (e) { finish(e) }
1977
+ })
1978
+ if (!closed) { outbound.bytes -= packet.length; outbound.messages-- }
1979
+ }
1980
+ } catch (e) {
1981
+ if (!closed) fail(e)
1982
+ } finally { flushing = false }
1983
+ }
1984
+ return {
1985
+ outbound,
1986
+ send(message) {
1987
+ if (closed || req.destroyed || !req.writable) throw new NativeSessionError(502, "native transport is no longer writable")
1988
+ const size = message.length + 5
1989
+ if (outbound.bytes + size > limits.outboundBytes) throw new NativeSessionError(503, "native outbound bytes limit reached")
1990
+ if (outbound.messages + 1 > limits.outboundMessages) throw new NativeSessionError(503, "native outbound messages limit reached")
1991
+ outbound.bytes += size
1992
+ outbound.messages++
1993
+ queue.push(frame(message))
1994
+ void flush()
1995
+ },
1996
+ close() {
1997
+ if (closed) return
1998
+ closed = true
1999
+ queue.length = 0
2000
+ outbound.bytes = 0
2001
+ outbound.messages = 0
2002
+ rejectWrite?.(new NativeSessionError(410, "native transport was cancelled"))
2003
+ close()
2004
+ },
2005
+ }
2006
+ }
2007
+
2008
+ // runNative opens a session-owned Run. Its background decoder survives the end
2009
+ // of each Chat segment; only terminal completion, failure, or cancellation
2010
+ // releases the transport.
2011
+ async function runNative({ tok, base, id, tools, conv, convID, signal, maxMode, params, native }) {
2012
+ const cid = convID || randomUUID()
2013
+ const since = Date.now()
2014
+ // Transport shutdown precedes final debt collection. Keep its cancellation
2015
+ // separate, and return any undelivered collection through the ledger's carry.
2016
+ const accounting = new AbortController()
2017
+ const book = ledger(tok, cid, since, accounting.signal, !!convID, { before: since })
2018
+ const debtKey = usageKey(tok, cid)
2019
+ let started = false, settled = false, deferred = null
2020
+ native.accounting = {
2021
+ started() { started = true },
2022
+ async settle() {
2023
+ const more = await book.owed(true)
2024
+ if (settled) {
2025
+ if (more) owe(debtKey, { carry: more })
2026
+ return null
2027
+ }
2028
+ deferred = more
2029
+ return more
2030
+ },
2031
+ delivered() {
2032
+ if (settled) return
2033
+ settled = true
2034
+ deferred = null
2035
+ book.done()
2036
+ accounting.abort()
2037
+ },
2038
+ abandon() {
2039
+ if (settled) return
2040
+ settled = true
2041
+ book.done()
2042
+ accounting.abort()
2043
+ if (deferred) owe(debtKey, { carry: deferred })
2044
+ deferred = null
2045
+ // Every handoff of this owner belongs to one Run. Only an abandoned
2046
+ // started/pending Run is owed; a final snapshot (including zero) is not.
2047
+ if (started && native.segments > 0 && !native.finalUsage && convID && canRead(tok)) {
2048
+ owe(debtKey, { since, n: 1, ready: Date.now() + STEP_WAIT.owedAt })
2049
+ }
2050
+ },
2051
+ }
2052
+ const { first, blobs } = buildRun(conv.msgs, conv.last, tools, id, cid, { maxMode, params })
2053
+ native.chatTools = tools
2054
+ native.attach(signal)
2055
+ if (native.error) {
2056
+ nativeSessions.forget(native)
2057
+ return { error: native.error }
2058
+ }
2059
+ const o = await open(
2060
+ base,
2061
+ {
2062
+ "content-type": "application/connect+proto",
2063
+ "connect-protocol-version": "1",
2064
+ authorization: `Bearer ${tok}`,
2065
+ "x-cursor-client-version": await clientVersion(),
2066
+ "x-cursor-client-type": "cli", // else Cursor adds a prompt of its own
2067
+ "x-ghost-mode": "true", // privacy mode: nothing kept for training
2068
+ "x-request-id": randomUUID(),
2069
+ // none of Cursor's own tools, only the caller's
2070
+ "x-cursor-agent-allowed-tools": "mcp_tool_call,get_mcp_tools_tool_call",
2071
+ },
2072
+ native.ac.signal,
2073
+ )
2074
+ if (o.error) {
2075
+ native.fail(o.error)
2076
+ nativeSessions.forget(native)
2077
+ return o
2078
+ }
2079
+ const { session, req } = o
2080
+ const close = () => {
2081
+ try { req.close(http2.constants.NGHTTP2_CANCEL) } catch {}
2082
+ session.close()
2083
+ session.destroy()
2084
+ }
2085
+ const transport = nativeTransport(req, close, (e) => native.fail({ status: e.status ?? 502, message: e.message }), native.limits)
2086
+ try {
2087
+ native.bind(transport, blobs, conv.estimate)
2088
+ if (native.error) {
2089
+ nativeSessions.forget(native)
2090
+ return { error: native.error }
2091
+ }
2092
+ native.send(first)
2093
+ native.accounting.started()
2094
+ if (o.status < 200 || o.status > 299) {
2095
+ const chunks = []
2096
+ for await (const c of req) chunks.push(c)
2097
+ native.fail(failure(o.status, Buffer.concat(chunks).toString("utf8").slice(0, 1 << 20)))
2098
+ nativeSessions.forget(native)
2099
+ return { error: native.error, h1: !!o.h1 }
2100
+ }
2101
+ native.pumpTask = (async () => {
2102
+ await native.pump(frames(req))
2103
+ })()
2104
+ const response = native.response()
2105
+ const it = response.events
2106
+ const head = await it.next()
2107
+ if (head.done || head.value.error) {
2108
+ const error = head.value?.error ?? { status: 502, message: "an empty reply" }
2109
+ await native.cancel(error.message)
2110
+ nativeSessions.forget(native)
2111
+ return { error, h1: !!o.h1 }
2112
+ }
2113
+ let firstEvent = head
2114
+ response.events = {
2115
+ next() {
2116
+ if (firstEvent) {
2117
+ const r = firstEvent
2118
+ firstEvent = null
2119
+ return Promise.resolve(r)
2120
+ }
2121
+ return it.next()
2122
+ },
2123
+ return: () => it.return(),
2124
+ [Symbol.asyncIterator]() { return this },
2125
+ }
2126
+ return response
2127
+ } catch (e) {
2128
+ native.fail({ status: e.status ?? 502, message: e.message })
2129
+ await native.pumpTask
2130
+ nativeSessions.forget(native)
2131
+ return { error: native.error, h1: !!o.h1 }
2132
+ }
2133
+ }
2134
+
1879
2135
  // runOnce is one Run of the chat on the agent API at base: an async
1880
2136
  // iterator of the answer's pieces, or the error it failed with before any.
1881
2137
  async function runOnce({ tok, base, id, tools, conv, convID, signal, maxMode, params }) {
@@ -2174,18 +2430,50 @@ function emptyWhy(how, seen, usage) {
2174
2430
  // exec answers what the server asks the client to run: a call of the
2175
2431
  // caller's tools is returned, the list of them is given, and anything else
2176
2432
  // is refused.
2177
- function exec(msg, tools, send, closeExec, refused) {
2178
- const es = fields(msg)
2433
+ function exec(msg, tools, send, closeExec, refused, native = false) {
2434
+ const es = native ? nativeFields(msg) : fields(msg)
2435
+ // ExecServerMessage.id is a proto3 uint32: an omitted field means zero.
2436
+ // Explicit malformed fields must not acquire that valid default.
2437
+ if (native && es.some((e) => e.num === 1 && (e.wire !== 0 || !Number.isInteger(e.n) || e.n < 0 || e.n > 0xffffffff)))
2438
+ throw new NativeSessionError(502, "invalid native exec request ID")
2179
2439
  const id = pbNum(es, 1)
2180
2440
  const execID = pbStr(es, 15)
2441
+ // Validate every MCP envelope before any exec can publish a call or send a
2442
+ // response. Differing duplicates have no supported selection/merge contract.
2443
+ let nativeCall, nativeUnavailable = false
2444
+ if (native) for (const e of es) if (e.num === 11) {
2445
+ if (e.wire !== 2) throw new NativeSessionError(502, "invalid native MCP argument envelope")
2446
+ const { fields: a, args } = nativeMcpArgs(e.data)
2447
+ const name = pbStr(a, 5) || pbStr(a, 1).replace(/^magpie-/, "")
2448
+ const provider = pbStr(a, 4)
2449
+ const server = pbStr(a, 9)
2450
+ if (provider && provider !== "magpie" || server && server !== "magpie") nativeUnavailable = true
2451
+ if (pbNum(a, 7) || field(a, 6)) nativeUnavailable = true
2452
+ if (!tools.some((t) => t.name === name)) nativeUnavailable = true
2453
+ if (!pbStr(a, 5) && pbStr(a, 1) !== "magpie-" + name) nativeUnavailable = true
2454
+ const call = { requestID: id, execID, nativeID: pbStr(a, 3), name, args }
2455
+ if (nativeCall && !isDeepStrictEqual(nativeCall, call)) throw new NativeSessionError(502, "ambiguous repeated native MCP argument envelopes")
2456
+ nativeCall ??= call
2457
+ }
2181
2458
  const answer = (num, res) => {
2182
2459
  send(pb().bytes(2, pb().varint(1, id).str(15, execID).bytes(num, res)).done())
2183
2460
  closeExec(id)
2184
2461
  }
2462
+ const unavailable = () => {
2463
+ const kind = es.find((e) => ![1, 15, 19, 55].includes(e.num))?.num ?? 0
2464
+ refused?.set(kind, (refused.get(kind) ?? 0) + 1)
2465
+ send(pb().bytes(5, pb().bytes(2, pb().varint(1, id).str(2, "not available"))).done())
2466
+ closeExec(id)
2467
+ return null
2468
+ }
2469
+ // A foreign/unregistered MCP or approval flow is unavailable to this bridge.
2470
+ // Refuse it like a native shell request, then let the model continue.
2471
+ if (nativeUnavailable) return unavailable()
2185
2472
  for (const e of es) {
2186
2473
  switch (e.num) {
2187
2474
  case 11: {
2188
2475
  // mcp_args: a call of the caller's tools
2476
+ if (native) return { ...nativeCall, args: JSON.stringify(nativeCall.args) }
2189
2477
  const a = fields(e.data)
2190
2478
  const args = {}
2191
2479
  for (const kv of a) {
@@ -2213,11 +2501,7 @@ function exec(msg, tools, send, closeExec, refused) {
2213
2501
  }
2214
2502
  }
2215
2503
  // a shell, a file read or an edit of Cursor's own: never run
2216
- const kind = es.find((e) => e.num !== 1 && e.num !== 15)?.num ?? 0
2217
- refused?.set(kind, (refused.get(kind) ?? 0) + 1)
2218
- send(pb().bytes(5, pb().bytes(2, pb().varint(1, id).str(2, "not available"))).done())
2219
- closeExec(id)
2220
- return null
2504
+ return unavailable()
2221
2505
  }
2222
2506
 
2223
2507
  // ---- chat completions ------------------------------------------------------------------
@@ -2258,67 +2542,117 @@ async function answerOf(auth, chat, signal, session = "") {
2258
2542
  } catch (e) {
2259
2543
  return errorResponse({ status: e.status ?? 401, message: e.message })
2260
2544
  }
2261
- const model = String(chat.model ?? "")
2262
- let raw = []
2263
- try {
2264
- raw = await usable(tok)
2265
- } catch {}
2266
- // fast only when asked: a service_tier of priority or fast is on; any
2267
- // other tier, or none, is off, unless the id is a fast one of Cursor's own
2268
- const tier = chat.service_tier
2269
- const fast = tier === "priority" || tier === "fast" ? true : tier ? false : undefined
2270
- const req = request(raw, model, chat.reasoning_effort ?? "", fast)
2271
- const id = req.id
2272
- const params = req.params
2273
- let maxMode = req.maxMode || maxOnly.has(id)
2545
+ if (signal?.aborted) return errorResponse({ status: 410, message: "the request was cancelled" })
2274
2546
  const tools = toolsOf(chat)
2275
- const conv = conversation(chat, tools)
2276
2547
  const convID = conversationID(chat, session)
2277
- let base = await agentURL(tok, false)
2278
- const once = () => runOnce({ tok, base, id, tools, conv, convID, signal, maxMode, params })
2279
- let r = await once()
2280
- if (r.error && regional(r.error.message)) {
2281
- // the team moved, or the config was kept from before: once more with
2282
- // what the config says now. A Run that went over HTTP/1.1 went to api2,
2283
- // not the region's agent host: once more over HTTP/2 there.
2284
- const viaH1 = !!r.h1
2285
- h1Until = 0
2286
- const fresh = await agentURL(tok, true)
2287
- if (fresh !== base || viaH1) {
2288
- base = fresh
2289
- r = await once()
2548
+ const prepared = nativeSessions.prepare(tok, chat, convID, tools)
2549
+ let r
2550
+ if (prepared.owner) r = prepared.owner.resume(prepared, signal)
2551
+ const model = String(chat.model ?? "")
2552
+ if (!r) {
2553
+ let raw = []
2554
+ try {
2555
+ raw = await usable(tok)
2556
+ } catch {}
2557
+ // fast only when asked: a service_tier of priority or fast is on; any
2558
+ // other tier, or none, is off, unless the id is a fast one of Cursor's own
2559
+ const tier = chat.service_tier
2560
+ const fast = tier === "priority" || tier === "fast" ? true : tier ? false : undefined
2561
+ const req = request(raw, model, chat.reasoning_effort ?? "", fast)
2562
+ const id = req.id
2563
+ const params = req.params
2564
+ let maxMode = req.maxMode || maxOnly.has(id)
2565
+ const conv = conversation(chat, tools)
2566
+ let base = await agentURL(tok, false)
2567
+ const once = async () => {
2568
+ // Retention is an optimization. Admission pressure or unavailable ownership
2569
+ // uses the original history builder/decoder without cancelling a live parent.
2570
+ const native = prepared.fallback ? null : nativeSessions.create(prepared, { id, maxMode, params }, { pb, exec, usageOf, failure })
2571
+ const args = { tok, base, id, tools, conv, convID, signal, maxMode, params }
2572
+ let response = native ? await runNative({ ...args, native }) : await runOnce(args)
2573
+ const rebuild = async () => {
2574
+ const fallback = await runOnce(args)
2575
+ fallback.diagnostic = "stateless; bootstrap=imported-history; reason=missing-batch-count"
2576
+ return fallback
2577
+ }
2578
+ // A missing native batch count may be retried once, only before Chat
2579
+ // output is exposed or any native result was submitted. Never repeat an
2580
+ // uncertain tool result write or duplicate a progressive stream's output.
2581
+ if (response.error?.retentionIneligible && !signal?.aborted) response = await rebuild()
2582
+ else if (native) response.rebuild = rebuild
2583
+ response.diagnostic ??= "stateless; bootstrap=imported-history; reason=" + (prepared.fallback || "retention capacity or reservation unavailable")
2584
+ return response
2290
2585
  }
2291
- }
2292
- if (r.error && !maxMode && maxRequired(r.error.message)) {
2293
- // a model Cursor serves only in Max Mode that neither list said so of:
2294
- // in Max Mode, as the CLI turns it on for such a model, and so from
2295
- // now on. An account Max Mode isn't open to is answered as Cursor
2296
- // answers that.
2297
- maxMode = true
2298
- maxOnly.add(id)
2299
2586
  r = await once()
2587
+ if (r.error && regional(r.error.message)) {
2588
+ // the team moved, or the config was kept from before: once more with
2589
+ // what the config says now. A Run that went over HTTP/1.1 went to api2,
2590
+ // not the region's agent host: once more over HTTP/2 there.
2591
+ const viaH1 = !!r.h1
2592
+ h1Until = 0
2593
+ const fresh = await agentURL(tok, true)
2594
+ if (fresh !== base || viaH1) {
2595
+ base = fresh
2596
+ r = await once()
2597
+ }
2598
+ }
2599
+ if (r.error && !maxMode && maxRequired(r.error.message)) {
2600
+ // a model Cursor serves only in Max Mode that neither list said so of:
2601
+ // in Max Mode, as the CLI turns it on for such a model, and so from
2602
+ // now on. An account Max Mode isn't open to is answered as Cursor
2603
+ // answers that.
2604
+ maxMode = true
2605
+ maxOnly.add(id)
2606
+ r = await once()
2607
+ }
2300
2608
  }
2301
2609
  if (r.error) return errorResponse(r.error)
2610
+ // A resumed owner may fail while submitting results. Surface that uncertain
2611
+ // write once; later history rebuilds never submit it again on that owner.
2302
2612
  const it = r.events
2613
+ let head = await it.next()
2614
+ if (head.value?.error) return errorResponse(head.value.error)
2615
+ let events = {
2616
+ next() {
2617
+ if (head) { const value = head; head = null; return Promise.resolve(value) }
2618
+ return it.next()
2619
+ },
2620
+ return: () => it.return?.(),
2621
+ [Symbol.asyncIterator]() { return this },
2622
+ }
2623
+ const responseHeaders = { "X-Magpie-Cursor-Continuity": r.diagnostic }
2303
2624
  const cid = "chatcmpl-" + randomBytes(12).toString("hex")
2304
2625
  const created = Math.floor(Date.now() / 1000)
2305
2626
 
2306
2627
  if (!chat.stream) {
2307
- const msg = { role: "assistant", content: "" }
2308
- let reasoning = ""
2309
- const calls = []
2310
- let stop = "stop"
2311
- let usage
2312
- for await (const e of it) {
2313
- if (e.error) return errorResponse(e.error)
2314
- if (e.text) msg.content += e.text
2315
- if (e.reasoning) reasoning += e.reasoning
2316
- if (e.tool) calls.push({ id: e.tool.id, type: "function", function: { name: e.tool.name, arguments: e.tool.args } })
2317
- if (e.stop) [stop, usage] = [e.stop, e.usage]
2628
+ while (true) {
2629
+ let retry = false
2630
+ const msg = { role: "assistant", content: "" }
2631
+ let reasoning = ""
2632
+ const calls = []
2633
+ let stop = "stop"
2634
+ let usage
2635
+ for await (const e of events) {
2636
+ if (e.error) {
2637
+ if (!e.error.retentionIneligible || !r.rebuild || signal?.aborted) return errorResponse(e.error)
2638
+ r = await r.rebuild()
2639
+ if (r.error) return errorResponse(r.error)
2640
+ events = r.events
2641
+ responseHeaders["X-Magpie-Cursor-Continuity"] = r.diagnostic
2642
+ retry = true
2643
+ break
2644
+ }
2645
+ if (e.text) msg.content += e.text
2646
+ if (e.reasoning) reasoning += e.reasoning
2647
+ if (e.tool) calls.push({ id: e.tool.id, type: "function", function: { name: e.tool.name, arguments: e.tool.args } })
2648
+ if (e.stop) [stop, usage] = [e.stop, e.usage]
2649
+ }
2650
+ if (retry) continue
2651
+ if (reasoning) msg.reasoning_content = reasoning
2652
+ if (calls.length) msg.tool_calls = calls
2653
+ r.delivered?.()
2654
+ return Response.json({ id: cid, object: "chat.completion", created, model, choices: [{ index: 0, message: msg, finish_reason: stop }], ...(usage ? { usage } : {}) }, { headers: responseHeaders })
2318
2655
  }
2319
- if (reasoning) msg.reasoning_content = reasoning
2320
- if (calls.length) msg.tool_calls = calls
2321
- return Response.json({ id: cid, object: "chat.completion", created, model, choices: [{ index: 0, message: msg, finish_reason: stop }], ...(usage ? { usage } : {}) })
2322
2656
  }
2323
2657
 
2324
2658
  const chunk = (delta, finish_reason = null, extra = {}) =>
@@ -2326,16 +2660,17 @@ async function answerOf(auth, chat, signal, session = "") {
2326
2660
  let began = false
2327
2661
  const stream = new ReadableStream({
2328
2662
  async pull(ctl) {
2329
- const r = await it.next()
2663
+ const next = await events.next()
2330
2664
  if (!began) {
2331
2665
  began = true
2332
2666
  ctl.enqueue(chunk({ role: "assistant", content: "" }))
2333
2667
  }
2334
- if (r.done) {
2668
+ if (next.done) {
2669
+ r.delivered?.()
2335
2670
  ctl.enqueue(enc.encode("data: [DONE]\n\n"))
2336
2671
  return ctl.close()
2337
2672
  }
2338
- const e = r.value
2673
+ const e = next.value
2339
2674
  if (e.text) ctl.enqueue(chunk({ content: e.text }))
2340
2675
  else if (e.reasoning) ctl.enqueue(chunk({ reasoning_content: e.reasoning }))
2341
2676
  else if (e.tool) ctl.enqueue(chunk({ tool_calls: [{ index: e.tool.index, id: e.tool.id, type: "function", function: { name: e.tool.name, arguments: e.tool.args } }] }))
@@ -2345,11 +2680,12 @@ async function answerOf(auth, chat, signal, session = "") {
2345
2680
  ctl.close()
2346
2681
  }
2347
2682
  },
2348
- cancel() {
2349
- it.return?.()
2683
+ async cancel() {
2684
+ await r.cancel?.()
2685
+ await events.return()
2350
2686
  },
2351
2687
  })
2352
- return new Response(stream, { status: 200, headers: { "Content-Type": "text/event-stream", "Cache-Control": "no-cache" } })
2688
+ return new Response(stream, { status: 200, headers: { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", ...responseHeaders } })
2353
2689
  }
2354
2690
 
2355
2691
  // ---- the plugin ---------------------------------------------------------------------------
@@ -2446,4 +2782,4 @@ export async function CursorAuthPlugin() {
2446
2782
  }
2447
2783
 
2448
2784
  // for tests
2449
- export const _internal = { onDemandOf,resetH1: () => (h1Until = 0), onH1: () => Date.now() < h1Until, H2, H1, h2Blocked, open, openH1, openH2, STEP_WAIT, stepUsage, usageKey, ledger, owed, flights, misses, unreadable, catalog, usable, request, modelsOf, tokensOf, maxRequired, conversationID, usageOf, sessionOf, SESSION, errorResponse, kept, poolBase, conversation, buildRun, decode, exec, fields, pb, pbValue, pbAny, frame, frames, failure, toolsOf, expiry }
2785
+ export const _internal = { nativeTransport, onDemandOf,resetH1: () => (h1Until = 0), onH1: () => Date.now() < h1Until, H2, H1, h2Blocked, open, openH1, openH2, STEP_WAIT, stepUsage, usageKey, ledger, owed, flights, misses, unreadable, catalog, usable, request, modelsOf, tokensOf, maxRequired, conversationID, usageOf, sessionOf, SESSION, errorResponse, kept, poolBase, conversation, buildRun, decode, exec, fields, pb, pbValue, pbAny, frame, frames, failure, toolsOf, expiry }