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

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,238 @@ 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
+ // waiters for room (KV replies), woken as writes complete. An eighth of
1943
+ // each bound stays free for heartbeats and exec answers, which never wait.
1944
+ const waiters = new Set()
1945
+ const spare = (n) => Math.floor(n / 8)
1946
+ const fits = (size) => outbound.messages === 0 || (outbound.messages + 1 + spare(limits.outboundMessages) <= limits.outboundMessages
1947
+ && outbound.bytes + size + spare(limits.outboundBytes) <= limits.outboundBytes)
1948
+ const wake = () => {
1949
+ for (const w of waiters) if (closed || fits(w.size)) w.done()
1950
+ }
1951
+ const flush = async () => {
1952
+ if (closed || flushing) return
1953
+ flushing = true
1954
+ try {
1955
+ while (!closed && queue.length) {
1956
+ const packet = queue.shift()
1957
+ await new Promise((resolve, reject) => {
1958
+ let written = false, drained = false, started = false, settled = false
1959
+ const finish = (error) => {
1960
+ if (settled) return
1961
+ settled = true
1962
+ rejectWrite = undefined
1963
+ req.removeListener("drain", drain)
1964
+ req.removeListener("error", failed)
1965
+ req.removeListener("close", lost)
1966
+ error ? reject(error) : resolve()
1967
+ }
1968
+ const check = () => { if (started && written && drained) finish() }
1969
+ const drain = () => { drained = true; check() }
1970
+ const failed = (e) => finish(e)
1971
+ const lost = () => finish(new NativeSessionError(502, "native transport closed during a write; its outcome is uncertain"))
1972
+ rejectWrite = finish
1973
+ req.once("drain", drain)
1974
+ req.once("error", failed)
1975
+ req.once("close", lost)
1976
+ try {
1977
+ drained = req.write(packet, (e) => {
1978
+ if (e) return finish(e)
1979
+ written = true
1980
+ check()
1981
+ }) || drained
1982
+ started = true
1983
+ if (drained) req.removeListener("drain", drain)
1984
+ check()
1985
+ } catch (e) { finish(e) }
1986
+ })
1987
+ if (!closed) { outbound.bytes -= packet.length; outbound.messages--; wake() }
1988
+ }
1989
+ } catch (e) {
1990
+ if (!closed) fail(e)
1991
+ } finally { flushing = false }
1992
+ }
1993
+ return {
1994
+ outbound,
1995
+ send(message) {
1996
+ if (closed || req.destroyed || !req.writable) throw new NativeSessionError(502, "native transport is no longer writable")
1997
+ const size = message.length + 5
1998
+ if (outbound.bytes + size > limits.outboundBytes) throw new NativeSessionError(503, "native outbound bytes limit reached")
1999
+ if (outbound.messages + 1 > limits.outboundMessages) throw new NativeSessionError(503, "native outbound messages limit reached")
2000
+ outbound.bytes += size
2001
+ outbound.messages++
2002
+ queue.push(frame(message))
2003
+ void flush()
2004
+ },
2005
+ // room resolves once a message of size fits the queue with the spare
2006
+ // kept, and fails the Run when the queue doesn't drain for writeWait.
2007
+ room(size) {
2008
+ if (closed || fits(size)) return Promise.resolve()
2009
+ return new Promise((resolve, reject) => {
2010
+ const w = {
2011
+ size,
2012
+ done() {
2013
+ waiters.delete(w)
2014
+ clearTimeout(w.timer)
2015
+ resolve()
2016
+ },
2017
+ }
2018
+ w.timer = setTimeout(() => {
2019
+ waiters.delete(w)
2020
+ const which = outbound.bytes + size + spare(limits.outboundBytes) > limits.outboundBytes ? "bytes" : "messages"
2021
+ reject(new NativeSessionError(503, `native outbound ${which} limit reached: the queue did not drain for ${Math.round(limits.writeWait / 1000)}s`))
2022
+ }, limits.writeWait)
2023
+ w.timer.unref?.()
2024
+ waiters.add(w)
2025
+ })
2026
+ },
2027
+ close() {
2028
+ if (closed) return
2029
+ closed = true
2030
+ queue.length = 0
2031
+ outbound.bytes = 0
2032
+ outbound.messages = 0
2033
+ wake()
2034
+ rejectWrite?.(new NativeSessionError(410, "native transport was cancelled"))
2035
+ close()
2036
+ },
2037
+ }
2038
+ }
2039
+
2040
+ // runNative opens a session-owned Run. Its background decoder survives the end
2041
+ // of each Chat segment; only terminal completion, failure, or cancellation
2042
+ // releases the transport.
2043
+ async function runNative({ tok, base, id, tools, conv, convID, signal, maxMode, params, native }) {
2044
+ const cid = convID || randomUUID()
2045
+ const since = Date.now()
2046
+ // Transport shutdown precedes final debt collection. Keep its cancellation
2047
+ // separate, and return any undelivered collection through the ledger's carry.
2048
+ const accounting = new AbortController()
2049
+ const book = ledger(tok, cid, since, accounting.signal, !!convID, { before: since })
2050
+ const debtKey = usageKey(tok, cid)
2051
+ let started = false, settled = false, deferred = null
2052
+ native.accounting = {
2053
+ started() { started = true },
2054
+ async settle() {
2055
+ const more = await book.owed(true)
2056
+ if (settled) {
2057
+ if (more) owe(debtKey, { carry: more })
2058
+ return null
2059
+ }
2060
+ deferred = more
2061
+ return more
2062
+ },
2063
+ delivered() {
2064
+ if (settled) return
2065
+ settled = true
2066
+ deferred = null
2067
+ book.done()
2068
+ accounting.abort()
2069
+ },
2070
+ abandon() {
2071
+ if (settled) return
2072
+ settled = true
2073
+ book.done()
2074
+ accounting.abort()
2075
+ if (deferred) owe(debtKey, { carry: deferred })
2076
+ deferred = null
2077
+ // Every handoff of this owner belongs to one Run. Only an abandoned
2078
+ // started/pending Run is owed; a final snapshot (including zero) is not.
2079
+ if (started && native.segments > 0 && !native.finalUsage && convID && canRead(tok)) {
2080
+ owe(debtKey, { since, n: 1, ready: Date.now() + STEP_WAIT.owedAt })
2081
+ }
2082
+ },
2083
+ }
2084
+ const { first, blobs } = buildRun(conv.msgs, conv.last, tools, id, cid, { maxMode, params })
2085
+ native.chatTools = tools
2086
+ native.attach(signal)
2087
+ if (native.error) {
2088
+ nativeSessions.forget(native)
2089
+ return { error: native.error }
2090
+ }
2091
+ const o = await open(
2092
+ base,
2093
+ {
2094
+ "content-type": "application/connect+proto",
2095
+ "connect-protocol-version": "1",
2096
+ authorization: `Bearer ${tok}`,
2097
+ "x-cursor-client-version": await clientVersion(),
2098
+ "x-cursor-client-type": "cli", // else Cursor adds a prompt of its own
2099
+ "x-ghost-mode": "true", // privacy mode: nothing kept for training
2100
+ "x-request-id": randomUUID(),
2101
+ // none of Cursor's own tools, only the caller's
2102
+ "x-cursor-agent-allowed-tools": "mcp_tool_call,get_mcp_tools_tool_call",
2103
+ },
2104
+ native.ac.signal,
2105
+ )
2106
+ if (o.error) {
2107
+ native.fail(o.error)
2108
+ nativeSessions.forget(native)
2109
+ return o
2110
+ }
2111
+ const { session, req } = o
2112
+ const close = () => {
2113
+ try { req.close(http2.constants.NGHTTP2_CANCEL) } catch {}
2114
+ session.close()
2115
+ session.destroy()
2116
+ }
2117
+ const transport = nativeTransport(req, close, (e) => native.fail({ status: e.status ?? 502, message: e.message }), native.limits)
2118
+ try {
2119
+ native.bind(transport, blobs, conv.estimate)
2120
+ if (native.error) {
2121
+ nativeSessions.forget(native)
2122
+ return { error: native.error }
2123
+ }
2124
+ native.send(first)
2125
+ native.accounting.started()
2126
+ if (o.status < 200 || o.status > 299) {
2127
+ const chunks = []
2128
+ for await (const c of req) chunks.push(c)
2129
+ native.fail(failure(o.status, Buffer.concat(chunks).toString("utf8").slice(0, 1 << 20)))
2130
+ nativeSessions.forget(native)
2131
+ return { error: native.error, h1: !!o.h1 }
2132
+ }
2133
+ native.pumpTask = (async () => {
2134
+ await native.pump(frames(req))
2135
+ })()
2136
+ const response = native.response()
2137
+ const it = response.events
2138
+ const head = await it.next()
2139
+ if (head.done || head.value.error) {
2140
+ const error = head.value?.error ?? { status: 502, message: "an empty reply" }
2141
+ await native.cancel(error.message)
2142
+ nativeSessions.forget(native)
2143
+ return { error, h1: !!o.h1 }
2144
+ }
2145
+ let firstEvent = head
2146
+ response.events = {
2147
+ next() {
2148
+ if (firstEvent) {
2149
+ const r = firstEvent
2150
+ firstEvent = null
2151
+ return Promise.resolve(r)
2152
+ }
2153
+ return it.next()
2154
+ },
2155
+ return: () => it.return(),
2156
+ [Symbol.asyncIterator]() { return this },
2157
+ }
2158
+ return response
2159
+ } catch (e) {
2160
+ native.fail({ status: e.status ?? 502, message: e.message })
2161
+ await native.pumpTask
2162
+ nativeSessions.forget(native)
2163
+ return { error: native.error, h1: !!o.h1 }
2164
+ }
2165
+ }
2166
+
1879
2167
  // runOnce is one Run of the chat on the agent API at base: an async
1880
2168
  // iterator of the answer's pieces, or the error it failed with before any.
1881
2169
  async function runOnce({ tok, base, id, tools, conv, convID, signal, maxMode, params }) {
@@ -1976,9 +2264,22 @@ async function runOnce({ tok, base, id, tools, conv, convID, signal, maxMode, pa
1976
2264
  // turn ends, or the model has made its tool calls.
1977
2265
  async function* decode(stream, { send, blobs, tools, estimate, close, stepUsage, owedUsage }) {
1978
2266
  let calls = 0
2267
+ // answered: the step's calls answered here (a refusal, the MCP tool
2268
+ // list), which Cursor's count of the step's calls takes in too (#142)
2269
+ let answered = 0
1979
2270
  // ended: Cursor said what the turn used
1980
2271
  let ended = false
1981
2272
  let listed = 0
2273
+ // stepDone is whether every call the step listed has come. A step whose
2274
+ // calls were all answered here goes on on Cursor's side, under a count of
2275
+ // its own.
2276
+ const stepDone = () => {
2277
+ if (!listed || calls + answered < listed) return false
2278
+ if (calls > 0) return true
2279
+ listed = 0
2280
+ answered = 0
2281
+ return false
2282
+ }
1982
2283
  let said = 0
1983
2284
  // wrote: the reply's text alone; a turn of thinking only says nothing
1984
2285
  let wrote = 0
@@ -2086,9 +2387,9 @@ async function* decode(stream, { send, blobs, tools, estimate, close, stepUsage,
2086
2387
  return
2087
2388
  }
2088
2389
  break
2089
- case 27: // how many tool calls the step makes, sent before them
2390
+ case 27: // how many tool calls the step makes, sent before or after them
2090
2391
  listed = pbNum(uf, 1)
2091
- if (listed > 0 && calls >= listed) {
2392
+ if (stepDone()) {
2092
2393
  yield await settled()
2093
2394
  return
2094
2395
  }
@@ -2104,8 +2405,8 @@ async function* decode(stream, { send, blobs, tools, estimate, close, stepUsage,
2104
2405
  if (call) {
2105
2406
  said += call.args.length
2106
2407
  yield { tool: { index: calls++, ...call } }
2107
- }
2108
- if (listed > 0 && calls >= listed) {
2408
+ } else if (!fields(m.data).some((e) => e.num === 10)) answered++
2409
+ if (stepDone()) {
2109
2410
  // every call made: the caller runs them
2110
2411
  yield await settled()
2111
2412
  return
@@ -2174,18 +2475,50 @@ function emptyWhy(how, seen, usage) {
2174
2475
  // exec answers what the server asks the client to run: a call of the
2175
2476
  // caller's tools is returned, the list of them is given, and anything else
2176
2477
  // is refused.
2177
- function exec(msg, tools, send, closeExec, refused) {
2178
- const es = fields(msg)
2478
+ function exec(msg, tools, send, closeExec, refused, native = false) {
2479
+ const es = native ? nativeFields(msg) : fields(msg)
2480
+ // ExecServerMessage.id is a proto3 uint32: an omitted field means zero.
2481
+ // Explicit malformed fields must not acquire that valid default.
2482
+ if (native && es.some((e) => e.num === 1 && (e.wire !== 0 || !Number.isInteger(e.n) || e.n < 0 || e.n > 0xffffffff)))
2483
+ throw new NativeSessionError(502, "invalid native exec request ID")
2179
2484
  const id = pbNum(es, 1)
2180
2485
  const execID = pbStr(es, 15)
2486
+ // Validate every MCP envelope before any exec can publish a call or send a
2487
+ // response. Differing duplicates have no supported selection/merge contract.
2488
+ let nativeCall, nativeUnavailable = false
2489
+ if (native) for (const e of es) if (e.num === 11) {
2490
+ if (e.wire !== 2) throw new NativeSessionError(502, "invalid native MCP argument envelope")
2491
+ const { fields: a, args } = nativeMcpArgs(e.data)
2492
+ const name = pbStr(a, 5) || pbStr(a, 1).replace(/^magpie-/, "")
2493
+ const provider = pbStr(a, 4)
2494
+ const server = pbStr(a, 9)
2495
+ if (provider && provider !== "magpie" || server && server !== "magpie") nativeUnavailable = true
2496
+ if (pbNum(a, 7) || field(a, 6)) nativeUnavailable = true
2497
+ if (!tools.some((t) => t.name === name)) nativeUnavailable = true
2498
+ if (!pbStr(a, 5) && pbStr(a, 1) !== "magpie-" + name) nativeUnavailable = true
2499
+ const call = { requestID: id, execID, nativeID: pbStr(a, 3), name, args }
2500
+ if (nativeCall && !isDeepStrictEqual(nativeCall, call)) throw new NativeSessionError(502, "ambiguous repeated native MCP argument envelopes")
2501
+ nativeCall ??= call
2502
+ }
2181
2503
  const answer = (num, res) => {
2182
2504
  send(pb().bytes(2, pb().varint(1, id).str(15, execID).bytes(num, res)).done())
2183
2505
  closeExec(id)
2184
2506
  }
2507
+ const unavailable = () => {
2508
+ const kind = es.find((e) => ![1, 15, 19, 55].includes(e.num))?.num ?? 0
2509
+ refused?.set(kind, (refused.get(kind) ?? 0) + 1)
2510
+ send(pb().bytes(5, pb().bytes(2, pb().varint(1, id).str(2, "not available"))).done())
2511
+ closeExec(id)
2512
+ return null
2513
+ }
2514
+ // A foreign/unregistered MCP or approval flow is unavailable to this bridge.
2515
+ // Refuse it like a native shell request, then let the model continue.
2516
+ if (nativeUnavailable) return unavailable()
2185
2517
  for (const e of es) {
2186
2518
  switch (e.num) {
2187
2519
  case 11: {
2188
2520
  // mcp_args: a call of the caller's tools
2521
+ if (native) return { ...nativeCall, args: JSON.stringify(nativeCall.args) }
2189
2522
  const a = fields(e.data)
2190
2523
  const args = {}
2191
2524
  for (const kv of a) {
@@ -2213,11 +2546,7 @@ function exec(msg, tools, send, closeExec, refused) {
2213
2546
  }
2214
2547
  }
2215
2548
  // 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
2549
+ return unavailable()
2221
2550
  }
2222
2551
 
2223
2552
  // ---- chat completions ------------------------------------------------------------------
@@ -2258,67 +2587,117 @@ async function answerOf(auth, chat, signal, session = "") {
2258
2587
  } catch (e) {
2259
2588
  return errorResponse({ status: e.status ?? 401, message: e.message })
2260
2589
  }
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)
2590
+ if (signal?.aborted) return errorResponse({ status: 410, message: "the request was cancelled" })
2274
2591
  const tools = toolsOf(chat)
2275
- const conv = conversation(chat, tools)
2276
2592
  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()
2593
+ const prepared = nativeSessions.prepare(tok, chat, convID, tools)
2594
+ let r
2595
+ if (prepared.owner) r = prepared.owner.resume(prepared, signal)
2596
+ const model = String(chat.model ?? "")
2597
+ if (!r) {
2598
+ let raw = []
2599
+ try {
2600
+ raw = await usable(tok)
2601
+ } catch {}
2602
+ // fast only when asked: a service_tier of priority or fast is on; any
2603
+ // other tier, or none, is off, unless the id is a fast one of Cursor's own
2604
+ const tier = chat.service_tier
2605
+ const fast = tier === "priority" || tier === "fast" ? true : tier ? false : undefined
2606
+ const req = request(raw, model, chat.reasoning_effort ?? "", fast)
2607
+ const id = req.id
2608
+ const params = req.params
2609
+ let maxMode = req.maxMode || maxOnly.has(id)
2610
+ const conv = conversation(chat, tools)
2611
+ let base = await agentURL(tok, false)
2612
+ const once = async () => {
2613
+ // Retention is an optimization. Admission pressure or unavailable ownership
2614
+ // uses the original history builder/decoder without cancelling a live parent.
2615
+ const native = prepared.fallback ? null : nativeSessions.create(prepared, { id, maxMode, params }, { pb, exec, usageOf, failure })
2616
+ const args = { tok, base, id, tools, conv, convID, signal, maxMode, params }
2617
+ let response = native ? await runNative({ ...args, native }) : await runOnce(args)
2618
+ const rebuild = async () => {
2619
+ const fallback = await runOnce(args)
2620
+ fallback.diagnostic = "stateless; bootstrap=imported-history; reason=missing-batch-count"
2621
+ return fallback
2622
+ }
2623
+ // A missing native batch count may be retried once, only before Chat
2624
+ // output is exposed or any native result was submitted. Never repeat an
2625
+ // uncertain tool result write or duplicate a progressive stream's output.
2626
+ if (response.error?.retentionIneligible && !signal?.aborted) response = await rebuild()
2627
+ else if (native) response.rebuild = rebuild
2628
+ response.diagnostic ??= "stateless; bootstrap=imported-history; reason=" + (prepared.fallback || "retention capacity or reservation unavailable")
2629
+ return response
2290
2630
  }
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
2631
  r = await once()
2632
+ if (r.error && regional(r.error.message)) {
2633
+ // the team moved, or the config was kept from before: once more with
2634
+ // what the config says now. A Run that went over HTTP/1.1 went to api2,
2635
+ // not the region's agent host: once more over HTTP/2 there.
2636
+ const viaH1 = !!r.h1
2637
+ h1Until = 0
2638
+ const fresh = await agentURL(tok, true)
2639
+ if (fresh !== base || viaH1) {
2640
+ base = fresh
2641
+ r = await once()
2642
+ }
2643
+ }
2644
+ if (r.error && !maxMode && maxRequired(r.error.message)) {
2645
+ // a model Cursor serves only in Max Mode that neither list said so of:
2646
+ // in Max Mode, as the CLI turns it on for such a model, and so from
2647
+ // now on. An account Max Mode isn't open to is answered as Cursor
2648
+ // answers that.
2649
+ maxMode = true
2650
+ maxOnly.add(id)
2651
+ r = await once()
2652
+ }
2300
2653
  }
2301
2654
  if (r.error) return errorResponse(r.error)
2655
+ // A resumed owner may fail while submitting results. Surface that uncertain
2656
+ // write once; later history rebuilds never submit it again on that owner.
2302
2657
  const it = r.events
2658
+ let head = await it.next()
2659
+ if (head.value?.error) return errorResponse(head.value.error)
2660
+ let events = {
2661
+ next() {
2662
+ if (head) { const value = head; head = null; return Promise.resolve(value) }
2663
+ return it.next()
2664
+ },
2665
+ return: () => it.return?.(),
2666
+ [Symbol.asyncIterator]() { return this },
2667
+ }
2668
+ const responseHeaders = { "X-Magpie-Cursor-Continuity": r.diagnostic }
2303
2669
  const cid = "chatcmpl-" + randomBytes(12).toString("hex")
2304
2670
  const created = Math.floor(Date.now() / 1000)
2305
2671
 
2306
2672
  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]
2673
+ while (true) {
2674
+ let retry = false
2675
+ const msg = { role: "assistant", content: "" }
2676
+ let reasoning = ""
2677
+ const calls = []
2678
+ let stop = "stop"
2679
+ let usage
2680
+ for await (const e of events) {
2681
+ if (e.error) {
2682
+ if (!e.error.retentionIneligible || !r.rebuild || signal?.aborted) return errorResponse(e.error)
2683
+ r = await r.rebuild()
2684
+ if (r.error) return errorResponse(r.error)
2685
+ events = r.events
2686
+ responseHeaders["X-Magpie-Cursor-Continuity"] = r.diagnostic
2687
+ retry = true
2688
+ break
2689
+ }
2690
+ if (e.text) msg.content += e.text
2691
+ if (e.reasoning) reasoning += e.reasoning
2692
+ if (e.tool) calls.push({ id: e.tool.id, type: "function", function: { name: e.tool.name, arguments: e.tool.args } })
2693
+ if (e.stop) [stop, usage] = [e.stop, e.usage]
2694
+ }
2695
+ if (retry) continue
2696
+ if (reasoning) msg.reasoning_content = reasoning
2697
+ if (calls.length) msg.tool_calls = calls
2698
+ r.delivered?.()
2699
+ return Response.json({ id: cid, object: "chat.completion", created, model, choices: [{ index: 0, message: msg, finish_reason: stop }], ...(usage ? { usage } : {}) }, { headers: responseHeaders })
2318
2700
  }
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
2701
  }
2323
2702
 
2324
2703
  const chunk = (delta, finish_reason = null, extra = {}) =>
@@ -2326,16 +2705,17 @@ async function answerOf(auth, chat, signal, session = "") {
2326
2705
  let began = false
2327
2706
  const stream = new ReadableStream({
2328
2707
  async pull(ctl) {
2329
- const r = await it.next()
2708
+ const next = await events.next()
2330
2709
  if (!began) {
2331
2710
  began = true
2332
2711
  ctl.enqueue(chunk({ role: "assistant", content: "" }))
2333
2712
  }
2334
- if (r.done) {
2713
+ if (next.done) {
2714
+ r.delivered?.()
2335
2715
  ctl.enqueue(enc.encode("data: [DONE]\n\n"))
2336
2716
  return ctl.close()
2337
2717
  }
2338
- const e = r.value
2718
+ const e = next.value
2339
2719
  if (e.text) ctl.enqueue(chunk({ content: e.text }))
2340
2720
  else if (e.reasoning) ctl.enqueue(chunk({ reasoning_content: e.reasoning }))
2341
2721
  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 +2725,12 @@ async function answerOf(auth, chat, signal, session = "") {
2345
2725
  ctl.close()
2346
2726
  }
2347
2727
  },
2348
- cancel() {
2349
- it.return?.()
2728
+ async cancel() {
2729
+ await r.cancel?.()
2730
+ await events.return()
2350
2731
  },
2351
2732
  })
2352
- return new Response(stream, { status: 200, headers: { "Content-Type": "text/event-stream", "Cache-Control": "no-cache" } })
2733
+ return new Response(stream, { status: 200, headers: { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", ...responseHeaders } })
2353
2734
  }
2354
2735
 
2355
2736
  // ---- the plugin ---------------------------------------------------------------------------
@@ -2446,4 +2827,4 @@ export async function CursorAuthPlugin() {
2446
2827
  }
2447
2828
 
2448
2829
  // 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 }
2830
+ 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 }