@magpie-community/opencode-cursor-auth 0.2.12 → 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/README.md +45 -2
- package/index.mjs +508 -123
- package/native-session.mjs +845 -0
- package/package.json +2 -2
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
|
|
25
|
-
//
|
|
26
|
-
//
|
|
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"
|
|
@@ -1358,17 +1360,18 @@ function usageOf(uf) {
|
|
|
1358
1360
|
// event: its step looks for it as before, and is guessed without it.
|
|
1359
1361
|
const STEP_WAIT = { first: 1500, every: 500, until: 6000, ask: 5000, skipAfter: 3, retryEvery: 10, owedAt: 5000, owedFor: 600_000 }
|
|
1360
1362
|
|
|
1361
|
-
// claimed are the usage events already counted, by conversation
|
|
1362
|
-
// so that two steps of one conversation never count the same one.
|
|
1363
|
+
// claimed are the usage events already counted, by account, conversation
|
|
1364
|
+
// and time, so that two steps of one conversation never count the same one.
|
|
1363
1365
|
const claimed = new Map()
|
|
1364
1366
|
|
|
1365
|
-
// owed are what each conversation still owes, by
|
|
1366
|
-
// whose events are to be counted,
|
|
1367
|
-
// for from ready on; and carry, a
|
|
1367
|
+
// owed are what each account's conversation still owes, by credential
|
|
1368
|
+
// fingerprint and conversation_id: n Runs whose events are to be counted,
|
|
1369
|
+
// the earliest started at since, looked for from ready on; and carry, a
|
|
1370
|
+
// count collected that no step gave out.
|
|
1368
1371
|
const owed = new Map()
|
|
1369
1372
|
|
|
1370
|
-
// flights are each
|
|
1371
|
-
// that ends the turn waits for.
|
|
1373
|
+
// flights are each account's conversation collections still going, which
|
|
1374
|
+
// a step that ends the turn waits for.
|
|
1372
1375
|
const flights = new Map()
|
|
1373
1376
|
|
|
1374
1377
|
// unreadable are the accounts (by token) whose usage events Cursor won't
|
|
@@ -1378,6 +1381,7 @@ const flights = new Map()
|
|
|
1378
1381
|
const unreadable = new Map()
|
|
1379
1382
|
const misses = new Map()
|
|
1380
1383
|
const tokKey = (tok) => createHash("sha256").update(tok).digest("hex")
|
|
1384
|
+
const usageKey = (tok, conv) => tokKey(tok) + "\0" + conv
|
|
1381
1385
|
function canRead(tok) {
|
|
1382
1386
|
const at = unreadable.get(tokKey(tok))
|
|
1383
1387
|
if (at === undefined) return true
|
|
@@ -1394,14 +1398,43 @@ function addUsage(a, b) {
|
|
|
1394
1398
|
return a
|
|
1395
1399
|
}
|
|
1396
1400
|
|
|
1397
|
-
// owe adds to conversation
|
|
1398
|
-
|
|
1399
|
-
|
|
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
|
+
}
|
|
1400
1416
|
if (n <= 0 && !carry) return
|
|
1401
|
-
const d = owed.get(
|
|
1402
|
-
if (!d) return void owed.set(
|
|
1403
|
-
d.
|
|
1404
|
-
|
|
1417
|
+
const d = owed.get(key)
|
|
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
|
+
}
|
|
1405
1438
|
d.ready = Math.min(d.ready, ready)
|
|
1406
1439
|
d.carry = addUsage(d.carry, carry)
|
|
1407
1440
|
}
|
|
@@ -1412,7 +1445,8 @@ function owe(conv, { since = Date.now(), n = 0, ready = Date.now(), carry = null
|
|
|
1412
1445
|
// when the account can't read them.
|
|
1413
1446
|
// Polling, they are looked for as STEP_WAIT says, from `first` on, until
|
|
1414
1447
|
// all show; else asked for once.
|
|
1415
|
-
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 } = {}) {
|
|
1449
|
+
const key = usageKey(tok, conv)
|
|
1416
1450
|
const end = Date.now() + STEP_WAIT.until
|
|
1417
1451
|
const wait = (ms) => new Promise((r) => {
|
|
1418
1452
|
const t = setTimeout(r, ms)
|
|
@@ -1423,6 +1457,7 @@ async function usageEvents(tok, conv, since, want, { poll = false, first = STEP_
|
|
|
1423
1457
|
let usage = null
|
|
1424
1458
|
let got = 0
|
|
1425
1459
|
const events = []
|
|
1460
|
+
const counts = groups?.map(() => 0)
|
|
1426
1461
|
if (poll) await wait(first)
|
|
1427
1462
|
while (!signal?.aborted) {
|
|
1428
1463
|
try {
|
|
@@ -1430,20 +1465,27 @@ async function usageEvents(tok, conv, since, want, { poll = false, first = STEP_
|
|
|
1430
1465
|
method: "POST",
|
|
1431
1466
|
headers: await headersFor(tok),
|
|
1432
1467
|
body: JSON.stringify({ teamId: 0, startDate: String(since - 60_000), endDate: String(Date.now() + 60_000), page: 1, pageSize: 50 }),
|
|
1433
|
-
signal: AbortSignal.timeout(STEP_WAIT.ask),
|
|
1468
|
+
signal: abortFetch && signal ? AbortSignal.any([signal, AbortSignal.timeout(STEP_WAIT.ask)]) : AbortSignal.timeout(STEP_WAIT.ask),
|
|
1434
1469
|
})
|
|
1435
1470
|
if (res.status >= 400 && res.status < 500 && res.status !== 429) {
|
|
1436
1471
|
// not this account's to read
|
|
1437
1472
|
unreadable.set(tokKey(tok), Date.now())
|
|
1438
|
-
return { usage, events, got, refused: true }
|
|
1473
|
+
return { usage, events, got, counts, refused: true }
|
|
1439
1474
|
}
|
|
1440
1475
|
const evs = res.ok ? ((await res.json())?.usageEventsDisplay ?? []) : []
|
|
1441
1476
|
const found = evs
|
|
1442
|
-
.filter((e) => e?.conversationId === conv && e.tokenUsage && num(e.timestamp) >= since - 1000 && !claimed.has(
|
|
1477
|
+
.filter((e) => e?.conversationId === conv && e.tokenUsage && num(e.timestamp) >= since - 1000 && !claimed.has(key + "\0" + e.timestamp))
|
|
1443
1478
|
.sort((a, b) => num(a.timestamp) - num(b.timestamp))
|
|
1444
|
-
.slice(0, want - got)
|
|
1445
1479
|
for (const ev of found) {
|
|
1446
|
-
|
|
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]++
|
|
1488
|
+
claimed.set(key + "\0" + ev.timestamp, Date.now())
|
|
1447
1489
|
const t = ev.tokenUsage
|
|
1448
1490
|
const u = { input: num(t.inputTokens), output: num(t.outputTokens), cacheRead: num(t.cacheReadTokens), cacheWrite: num(t.cacheWriteTokens), reasoning: 0 }
|
|
1449
1491
|
usage = addUsage(usage ?? ZERO(), u)
|
|
@@ -1455,7 +1497,7 @@ async function usageEvents(tok, conv, since, want, { poll = false, first = STEP_
|
|
|
1455
1497
|
if (!poll || Date.now() + STEP_WAIT.every > end) break
|
|
1456
1498
|
await wait(STEP_WAIT.every)
|
|
1457
1499
|
}
|
|
1458
|
-
return { usage, events, got, refused: false }
|
|
1500
|
+
return { usage, events, got, counts, refused: false }
|
|
1459
1501
|
}
|
|
1460
1502
|
|
|
1461
1503
|
// stepUsage is the usage of the Run of conversation conv started at since,
|
|
@@ -1464,31 +1506,45 @@ async function stepUsage(tok, conv, since, signal) {
|
|
|
1464
1506
|
return (await usageEvents(tok, conv, since, 1, { poll: true, signal })).usage
|
|
1465
1507
|
}
|
|
1466
1508
|
|
|
1467
|
-
// collect takes
|
|
1468
|
-
// when poll): what was carried and what was found (usage),
|
|
1469
|
-
// carried alone (carry) and the events found (events), and whether any is
|
|
1509
|
+
// collect takes the account's debt for conversation conv and asks for its
|
|
1510
|
+
// events (polling when poll): what was carried and what was found (usage),
|
|
1511
|
+
// what was carried alone (carry) and the events found (events), and whether any is
|
|
1470
1512
|
// still owed (short), which is owed on. What comes back once the Run that
|
|
1471
1513
|
// asked is over (over() true) is carried to the next instead.
|
|
1472
|
-
function collect(tok, conv, { poll = false, first, signal, over = () => false } = {}) {
|
|
1473
|
-
const
|
|
1514
|
+
function collect(tok, conv, { poll = false, first, signal, over = () => false, abortFetch = false } = {}) {
|
|
1515
|
+
const key = usageKey(tok, conv)
|
|
1516
|
+
const d = owed.get(key)
|
|
1474
1517
|
if (!d) return Promise.resolve({ usage: null, carry: null, events: [], short: false })
|
|
1475
|
-
owed.delete(
|
|
1518
|
+
owed.delete(key)
|
|
1476
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)
|
|
1477
1522
|
let r = { usage: null, events: [], got: 0 }
|
|
1478
|
-
if (
|
|
1479
|
-
|
|
1480
|
-
|
|
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
|
+
}
|
|
1481
1534
|
const carry = d.carry && { ...d.carry }
|
|
1482
1535
|
const usage = addUsage(d.carry, r.usage)
|
|
1483
1536
|
if (!over()) return { usage, carry, events: r.events ?? [], short }
|
|
1484
|
-
owe(
|
|
1537
|
+
owe(key, { carry: usage })
|
|
1485
1538
|
return { usage: null, carry: null, events: [], short }
|
|
1486
1539
|
})()
|
|
1487
|
-
|
|
1488
|
-
|
|
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
|
|
1543
|
+
const set = flights.get(key) ?? new Set()
|
|
1544
|
+
flights.set(key, set.add(p))
|
|
1489
1545
|
p.finally(() => {
|
|
1490
1546
|
set.delete(p)
|
|
1491
|
-
if (!set.size && flights.get(
|
|
1547
|
+
if (!set.size && flights.get(key) === set) flights.delete(key)
|
|
1492
1548
|
})
|
|
1493
1549
|
return p
|
|
1494
1550
|
}
|
|
@@ -1513,13 +1569,18 @@ function collect(tok, conv, { poll = false, first, signal, over = () => false }
|
|
|
1513
1569
|
// owed(), apart. A Run of its conversation begins only once the one
|
|
1514
1570
|
// before has ended, so the two never meet; were the time off, the step's
|
|
1515
1571
|
// count would be told in the wrong field, never twice nor lost.
|
|
1516
|
-
function ledger(tok, conv, since, signal, kept) {
|
|
1572
|
+
function ledger(tok, conv, since, signal, kept, { before } = {}) {
|
|
1517
1573
|
const key = tokKey(tok)
|
|
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
|
+
}
|
|
1518
1579
|
let mine = null // this Run's own, collected, not given out yet
|
|
1519
1580
|
let theirs = null // earlier Runs', collected, not given out yet
|
|
1520
1581
|
let over = false
|
|
1521
1582
|
const gather = (opts) =>
|
|
1522
|
-
collect(tok, conv, { ...opts, over: () => over }).then((r) => {
|
|
1583
|
+
collect(tok, conv, { ...opts, over: () => over, abortFetch: before !== undefined }).then((r) => {
|
|
1523
1584
|
theirs = addUsage(theirs, r.carry)
|
|
1524
1585
|
for (const e of r.events ?? []) {
|
|
1525
1586
|
if (e.at >= since - 1000) mine = addUsage(mine, e.usage)
|
|
@@ -1528,7 +1589,7 @@ function ledger(tok, conv, since, signal, kept) {
|
|
|
1528
1589
|
return r
|
|
1529
1590
|
})
|
|
1530
1591
|
let collecting = null
|
|
1531
|
-
const debt = kept ? owed.get(
|
|
1592
|
+
const debt = kept ? owed.get(debtKey) : undefined
|
|
1532
1593
|
const timer = debt ? setTimeout(() => (collecting = gather()), Math.max(0, debt.ready - Date.now())) : null
|
|
1533
1594
|
timer?.unref?.()
|
|
1534
1595
|
const takeMine = () => {
|
|
@@ -1552,7 +1613,7 @@ function ledger(tok, conv, since, signal, kept) {
|
|
|
1552
1613
|
else if (!r.refused) misses.set(key, m + 1)
|
|
1553
1614
|
return r.usage ?? null
|
|
1554
1615
|
}
|
|
1555
|
-
owe(
|
|
1616
|
+
owe(debtKey, { since, n: 1, ready: Date.now() + STEP_WAIT.owedAt })
|
|
1556
1617
|
if (waits) {
|
|
1557
1618
|
const r = await gather({ poll: true, signal })
|
|
1558
1619
|
if (!canRead(tok)) return null
|
|
@@ -1565,8 +1626,8 @@ function ledger(tok, conv, since, signal, kept) {
|
|
|
1565
1626
|
if (!kept) return null
|
|
1566
1627
|
if (final) {
|
|
1567
1628
|
await collecting
|
|
1568
|
-
await Promise.all([...(flights.get(
|
|
1569
|
-
if (owed.has(
|
|
1629
|
+
await Promise.all([...(flights.get(debtKey) ?? [])])
|
|
1630
|
+
if (owed.has(debtKey)) await gather({ poll: owed.get(debtKey).n > 0, first: 0, signal })
|
|
1570
1631
|
}
|
|
1571
1632
|
return take()
|
|
1572
1633
|
},
|
|
@@ -1574,7 +1635,7 @@ function ledger(tok, conv, since, signal, kept) {
|
|
|
1574
1635
|
over = true
|
|
1575
1636
|
clearTimeout(timer)
|
|
1576
1637
|
const u = take()
|
|
1577
|
-
if (u) owe(
|
|
1638
|
+
if (u) owe(debtKey, { carry: u })
|
|
1578
1639
|
},
|
|
1579
1640
|
}
|
|
1580
1641
|
}
|
|
@@ -1666,12 +1727,21 @@ async function agentURL(tok, fresh) {
|
|
|
1666
1727
|
// connection that failed (refused, no network) or a stream the server
|
|
1667
1728
|
// refused leaves the next Run to try HTTP/2 again.
|
|
1668
1729
|
let h1Until = 0
|
|
1730
|
+
const cancelledOpen = () => ({ error: { status: 410, message: "the request was cancelled" }, h2Failed: false })
|
|
1669
1731
|
async function open(base, headers, signal) {
|
|
1732
|
+
if (signal?.aborted) return cancelledOpen()
|
|
1670
1733
|
if (Date.now() >= h1Until) {
|
|
1671
1734
|
const o = await openH2(base, headers, signal)
|
|
1735
|
+
// The head can arrive just before abort, before this continuation owns it.
|
|
1736
|
+
if (signal?.aborted) {
|
|
1737
|
+
o.req?.close(http2.constants.NGHTTP2_CANCEL)
|
|
1738
|
+
o.session?.destroy()
|
|
1739
|
+
return cancelledOpen()
|
|
1740
|
+
}
|
|
1672
1741
|
if (!o.h2Failed) return o
|
|
1673
1742
|
if (o.blocked) h1Until = Date.now() + H2.retryAfter
|
|
1674
1743
|
}
|
|
1744
|
+
if (signal?.aborted) return cancelledOpen()
|
|
1675
1745
|
return openH1(headers, signal)
|
|
1676
1746
|
}
|
|
1677
1747
|
|
|
@@ -1702,6 +1772,7 @@ function h2Blocked(e) {
|
|
|
1702
1772
|
// than by being cancelled, says h2Failed, and blocked when h2Blocked says
|
|
1703
1773
|
// so of the failure.
|
|
1704
1774
|
function openH2(base, headers, signal) {
|
|
1775
|
+
if (signal?.aborted) return Promise.resolve(cancelledOpen())
|
|
1705
1776
|
return new Promise((resolve) => {
|
|
1706
1777
|
const u = new URL(base)
|
|
1707
1778
|
const session = http2.connect(u.origin)
|
|
@@ -1710,9 +1781,11 @@ function openH2(base, headers, signal) {
|
|
|
1710
1781
|
if (done) return
|
|
1711
1782
|
done = true
|
|
1712
1783
|
clearTimeout(timer)
|
|
1784
|
+
signal?.removeEventListener("abort", abort)
|
|
1713
1785
|
session.destroy()
|
|
1714
|
-
resolve({ error: { status: 502, message: e?.message ?? String(e) }, h2Failed, blocked: h2Failed && h2Blocked(e) })
|
|
1786
|
+
resolve({ error: { status: h2Failed ? 502 : 410, message: e?.message ?? String(e) }, h2Failed, blocked: h2Failed && h2Blocked(e) })
|
|
1715
1787
|
}
|
|
1788
|
+
const abort = () => fail(new Error("the request was cancelled"), false)
|
|
1716
1789
|
const timer = setTimeout(() => fail(Object.assign(new Error(`no answer over HTTP/2 in ${H2.wait / 1000}s`), { h2Timeout: true })), H2.wait)
|
|
1717
1790
|
session.on("error", fail)
|
|
1718
1791
|
const req = session.request({
|
|
@@ -1728,9 +1801,11 @@ function openH2(base, headers, signal) {
|
|
|
1728
1801
|
if (done) return
|
|
1729
1802
|
done = true
|
|
1730
1803
|
clearTimeout(timer)
|
|
1804
|
+
signal?.removeEventListener("abort", abort)
|
|
1731
1805
|
resolve({ session, req, status: Number(h[":status"]) })
|
|
1732
1806
|
})
|
|
1733
|
-
signal?.addEventListener("abort",
|
|
1807
|
+
signal?.addEventListener("abort", abort, { once: true })
|
|
1808
|
+
if (signal?.aborted) abort()
|
|
1734
1809
|
})
|
|
1735
1810
|
}
|
|
1736
1811
|
|
|
@@ -1750,23 +1825,29 @@ function connectCode(status) {
|
|
|
1750
1825
|
// ends the stream with Connect's end frame, which runOnce reads as the
|
|
1751
1826
|
// Run's own.
|
|
1752
1827
|
function openH1(headers, signal) {
|
|
1828
|
+
if (signal?.aborted) return Promise.resolve(cancelledOpen())
|
|
1753
1829
|
const ac = new AbortController()
|
|
1754
|
-
signal?.addEventListener("abort", () => ac.abort(), { once: true })
|
|
1755
1830
|
const id = headers["x-request-id"]
|
|
1756
1831
|
const h = { ...headers, "accept-encoding": "identity", "x-cursor-streaming": "true" }
|
|
1757
1832
|
let ended = false
|
|
1758
1833
|
let seq = 0
|
|
1759
|
-
let
|
|
1834
|
+
let reader
|
|
1760
1835
|
const req = new Duplex({
|
|
1761
1836
|
read() {},
|
|
1762
1837
|
write(chunk, _enc, cb) {
|
|
1763
1838
|
// runOnce writes one whole frame each time
|
|
1764
1839
|
const msg = Buffer.from(chunk).subarray(5)
|
|
1765
1840
|
const n = seq++
|
|
1766
|
-
|
|
1767
|
-
|
|
1841
|
+
// Keep the write buffered until its append finishes, so callbacks and
|
|
1842
|
+
// drain reflect the pending upload rather than an unbounded promise chain.
|
|
1843
|
+
append(msg, n).then(() => cb(), (e) => cb(e))
|
|
1768
1844
|
},
|
|
1769
1845
|
})
|
|
1846
|
+
const cleanup = () => {
|
|
1847
|
+
signal?.removeEventListener("abort", close)
|
|
1848
|
+
ac.abort()
|
|
1849
|
+
reader?.cancel().catch(() => {})
|
|
1850
|
+
}
|
|
1770
1851
|
const end = (body) => {
|
|
1771
1852
|
if (ended || req.destroyed) return
|
|
1772
1853
|
ended = true
|
|
@@ -1774,7 +1855,7 @@ function openH1(headers, signal) {
|
|
|
1774
1855
|
f[0] = 2
|
|
1775
1856
|
req.push(f)
|
|
1776
1857
|
req.push(null)
|
|
1777
|
-
|
|
1858
|
+
cleanup()
|
|
1778
1859
|
}
|
|
1779
1860
|
const failed = async (res) => {
|
|
1780
1861
|
const text = await res.text().catch(() => "")
|
|
@@ -1788,6 +1869,7 @@ function openH1(headers, signal) {
|
|
|
1788
1869
|
// (more time for a large one) ends the Run, as cursor-agent's
|
|
1789
1870
|
// bidi_append_deadline_exceeded does: the messages after it wait on it.
|
|
1790
1871
|
const append = async (msg, n) => {
|
|
1872
|
+
if (ended || ac.signal.aborted) return
|
|
1791
1873
|
const body = pb().str(1, msg.toString("hex")).bytes(2, pb().str(1, id).done()).varint(3, n).done()
|
|
1792
1874
|
const ms = H1.appendWait + Math.ceil(body.length / 65536) * 1000
|
|
1793
1875
|
const deadline = AbortSignal.timeout(ms)
|
|
@@ -1798,6 +1880,7 @@ function openH1(headers, signal) {
|
|
|
1798
1880
|
body,
|
|
1799
1881
|
signal: AbortSignal.any([ac.signal, deadline]),
|
|
1800
1882
|
})
|
|
1883
|
+
if (ended || ac.signal.aborted) { await res.body?.cancel().catch(() => {}); return }
|
|
1801
1884
|
if (!res.ok) return failed(res)
|
|
1802
1885
|
await res.arrayBuffer()
|
|
1803
1886
|
} catch (e) {
|
|
@@ -1806,6 +1889,16 @@ function openH1(headers, signal) {
|
|
|
1806
1889
|
end(JSON.stringify({ error: { code: "unavailable", message: `BidiAppend: ${e?.message ?? e}` } }))
|
|
1807
1890
|
}
|
|
1808
1891
|
}
|
|
1892
|
+
const close = () => {
|
|
1893
|
+
ended = true
|
|
1894
|
+
cleanup()
|
|
1895
|
+
req.destroy()
|
|
1896
|
+
}
|
|
1897
|
+
signal?.addEventListener("abort", close, { once: true })
|
|
1898
|
+
if (signal?.aborted) {
|
|
1899
|
+
close()
|
|
1900
|
+
return Promise.resolve(cancelledOpen())
|
|
1901
|
+
}
|
|
1809
1902
|
;(async () => {
|
|
1810
1903
|
try {
|
|
1811
1904
|
const res = await fetch(API + "/agent.v1.AgentService/RunSSE", {
|
|
@@ -1814,25 +1907,229 @@ function openH1(headers, signal) {
|
|
|
1814
1907
|
body: frame(pb().str(1, id).done()),
|
|
1815
1908
|
signal: ac.signal,
|
|
1816
1909
|
})
|
|
1910
|
+
if (ended || ac.signal.aborted) { await res.body?.cancel().catch(() => {}); return }
|
|
1817
1911
|
if (!res.ok) return failed(res)
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
req.
|
|
1912
|
+
reader = res.body.getReader()
|
|
1913
|
+
try {
|
|
1914
|
+
while (!ended && !req.destroyed) {
|
|
1915
|
+
const { value, done } = await reader.read()
|
|
1916
|
+
if (done) break
|
|
1917
|
+
if (!ended && !req.destroyed) req.push(Buffer.from(value))
|
|
1918
|
+
}
|
|
1919
|
+
} finally {
|
|
1920
|
+
reader.releaseLock()
|
|
1921
|
+
reader = undefined
|
|
1821
1922
|
}
|
|
1822
1923
|
if (!ended && !req.destroyed) {
|
|
1823
1924
|
ended = true
|
|
1824
1925
|
req.push(null)
|
|
1926
|
+
cleanup()
|
|
1825
1927
|
}
|
|
1826
1928
|
} catch (e) {
|
|
1827
1929
|
if (!ac.signal.aborted) end(JSON.stringify({ error: { code: "unavailable", message: `RunSSE: ${e?.message ?? e}` } }))
|
|
1828
1930
|
}
|
|
1829
1931
|
})()
|
|
1932
|
+
return Promise.resolve(signal?.aborted ? cancelledOpen() : { session: { close, destroy: close }, req: Object.assign(req, { close }), status: 200, h1: true })
|
|
1933
|
+
}
|
|
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
|
|
1830
2080
|
const close = () => {
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
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 }
|
|
1834
2132
|
}
|
|
1835
|
-
return Promise.resolve({ session: { close, destroy: close }, req: Object.assign(req, { close }), status: 200, h1: true })
|
|
1836
2133
|
}
|
|
1837
2134
|
|
|
1838
2135
|
// runOnce is one Run of the chat on the agent API at base: an async
|
|
@@ -1865,14 +2162,15 @@ async function runOnce({ tok, base, id, tools, conv, convID, signal, maxMode, pa
|
|
|
1865
2162
|
if (o.error) return book.done(), o
|
|
1866
2163
|
const { session, req } = o
|
|
1867
2164
|
let closed = false
|
|
2165
|
+
let heartbeat
|
|
1868
2166
|
const send = (msg) => {
|
|
1869
2167
|
if (!closed && !req.destroyed && req.writable) req.write(frame(msg))
|
|
1870
2168
|
}
|
|
1871
|
-
const heartbeat = setInterval(() => send(pb().bytes(7, EMPTY).done()), 5000) // the client is still there, as the CLI says
|
|
1872
2169
|
const close = () => {
|
|
1873
2170
|
if (closed) return
|
|
1874
2171
|
closed = true
|
|
1875
2172
|
clearInterval(heartbeat)
|
|
2173
|
+
signal?.removeEventListener("abort", close)
|
|
1876
2174
|
try {
|
|
1877
2175
|
req.close(http2.constants.NGHTTP2_CANCEL)
|
|
1878
2176
|
} catch {}
|
|
@@ -1880,6 +2178,13 @@ async function runOnce({ tok, base, id, tools, conv, convID, signal, maxMode, pa
|
|
|
1880
2178
|
setTimeout(() => session.destroy(), 1000).unref?.()
|
|
1881
2179
|
}
|
|
1882
2180
|
signal?.addEventListener("abort", close, { once: true })
|
|
2181
|
+
// Abort can win after open's head check but before this Run takes ownership.
|
|
2182
|
+
if (signal?.aborted) {
|
|
2183
|
+
close()
|
|
2184
|
+
book.done()
|
|
2185
|
+
return cancelledOpen()
|
|
2186
|
+
}
|
|
2187
|
+
heartbeat = setInterval(() => send(pb().bytes(7, EMPTY).done()), 5000) // the client is still there, as the CLI says
|
|
1883
2188
|
send(first)
|
|
1884
2189
|
if (o.status < 200 || o.status > 299) {
|
|
1885
2190
|
const chunks = []
|
|
@@ -2125,18 +2430,50 @@ function emptyWhy(how, seen, usage) {
|
|
|
2125
2430
|
// exec answers what the server asks the client to run: a call of the
|
|
2126
2431
|
// caller's tools is returned, the list of them is given, and anything else
|
|
2127
2432
|
// is refused.
|
|
2128
|
-
function exec(msg, tools, send, closeExec, refused) {
|
|
2129
|
-
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")
|
|
2130
2439
|
const id = pbNum(es, 1)
|
|
2131
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
|
+
}
|
|
2132
2458
|
const answer = (num, res) => {
|
|
2133
2459
|
send(pb().bytes(2, pb().varint(1, id).str(15, execID).bytes(num, res)).done())
|
|
2134
2460
|
closeExec(id)
|
|
2135
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()
|
|
2136
2472
|
for (const e of es) {
|
|
2137
2473
|
switch (e.num) {
|
|
2138
2474
|
case 11: {
|
|
2139
2475
|
// mcp_args: a call of the caller's tools
|
|
2476
|
+
if (native) return { ...nativeCall, args: JSON.stringify(nativeCall.args) }
|
|
2140
2477
|
const a = fields(e.data)
|
|
2141
2478
|
const args = {}
|
|
2142
2479
|
for (const kv of a) {
|
|
@@ -2164,11 +2501,7 @@ function exec(msg, tools, send, closeExec, refused) {
|
|
|
2164
2501
|
}
|
|
2165
2502
|
}
|
|
2166
2503
|
// a shell, a file read or an edit of Cursor's own: never run
|
|
2167
|
-
|
|
2168
|
-
refused?.set(kind, (refused.get(kind) ?? 0) + 1)
|
|
2169
|
-
send(pb().bytes(5, pb().bytes(2, pb().varint(1, id).str(2, "not available"))).done())
|
|
2170
|
-
closeExec(id)
|
|
2171
|
-
return null
|
|
2504
|
+
return unavailable()
|
|
2172
2505
|
}
|
|
2173
2506
|
|
|
2174
2507
|
// ---- chat completions ------------------------------------------------------------------
|
|
@@ -2209,67 +2542,117 @@ async function answerOf(auth, chat, signal, session = "") {
|
|
|
2209
2542
|
} catch (e) {
|
|
2210
2543
|
return errorResponse({ status: e.status ?? 401, message: e.message })
|
|
2211
2544
|
}
|
|
2212
|
-
|
|
2213
|
-
let raw = []
|
|
2214
|
-
try {
|
|
2215
|
-
raw = await usable(tok)
|
|
2216
|
-
} catch {}
|
|
2217
|
-
// fast only when asked: a service_tier of priority or fast is on; any
|
|
2218
|
-
// other tier, or none, is off, unless the id is a fast one of Cursor's own
|
|
2219
|
-
const tier = chat.service_tier
|
|
2220
|
-
const fast = tier === "priority" || tier === "fast" ? true : tier ? false : undefined
|
|
2221
|
-
const req = request(raw, model, chat.reasoning_effort ?? "", fast)
|
|
2222
|
-
const id = req.id
|
|
2223
|
-
const params = req.params
|
|
2224
|
-
let maxMode = req.maxMode || maxOnly.has(id)
|
|
2545
|
+
if (signal?.aborted) return errorResponse({ status: 410, message: "the request was cancelled" })
|
|
2225
2546
|
const tools = toolsOf(chat)
|
|
2226
|
-
const conv = conversation(chat, tools)
|
|
2227
2547
|
const convID = conversationID(chat, session)
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
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
|
|
2241
2585
|
}
|
|
2242
|
-
}
|
|
2243
|
-
if (r.error && !maxMode && maxRequired(r.error.message)) {
|
|
2244
|
-
// a model Cursor serves only in Max Mode that neither list said so of:
|
|
2245
|
-
// in Max Mode, as the CLI turns it on for such a model, and so from
|
|
2246
|
-
// now on. An account Max Mode isn't open to is answered as Cursor
|
|
2247
|
-
// answers that.
|
|
2248
|
-
maxMode = true
|
|
2249
|
-
maxOnly.add(id)
|
|
2250
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
|
+
}
|
|
2251
2608
|
}
|
|
2252
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.
|
|
2253
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 }
|
|
2254
2624
|
const cid = "chatcmpl-" + randomBytes(12).toString("hex")
|
|
2255
2625
|
const created = Math.floor(Date.now() / 1000)
|
|
2256
2626
|
|
|
2257
2627
|
if (!chat.stream) {
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
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 })
|
|
2269
2655
|
}
|
|
2270
|
-
if (reasoning) msg.reasoning_content = reasoning
|
|
2271
|
-
if (calls.length) msg.tool_calls = calls
|
|
2272
|
-
return Response.json({ id: cid, object: "chat.completion", created, model, choices: [{ index: 0, message: msg, finish_reason: stop }], ...(usage ? { usage } : {}) })
|
|
2273
2656
|
}
|
|
2274
2657
|
|
|
2275
2658
|
const chunk = (delta, finish_reason = null, extra = {}) =>
|
|
@@ -2277,16 +2660,17 @@ async function answerOf(auth, chat, signal, session = "") {
|
|
|
2277
2660
|
let began = false
|
|
2278
2661
|
const stream = new ReadableStream({
|
|
2279
2662
|
async pull(ctl) {
|
|
2280
|
-
const
|
|
2663
|
+
const next = await events.next()
|
|
2281
2664
|
if (!began) {
|
|
2282
2665
|
began = true
|
|
2283
2666
|
ctl.enqueue(chunk({ role: "assistant", content: "" }))
|
|
2284
2667
|
}
|
|
2285
|
-
if (
|
|
2668
|
+
if (next.done) {
|
|
2669
|
+
r.delivered?.()
|
|
2286
2670
|
ctl.enqueue(enc.encode("data: [DONE]\n\n"))
|
|
2287
2671
|
return ctl.close()
|
|
2288
2672
|
}
|
|
2289
|
-
const e =
|
|
2673
|
+
const e = next.value
|
|
2290
2674
|
if (e.text) ctl.enqueue(chunk({ content: e.text }))
|
|
2291
2675
|
else if (e.reasoning) ctl.enqueue(chunk({ reasoning_content: e.reasoning }))
|
|
2292
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 } }] }))
|
|
@@ -2296,11 +2680,12 @@ async function answerOf(auth, chat, signal, session = "") {
|
|
|
2296
2680
|
ctl.close()
|
|
2297
2681
|
}
|
|
2298
2682
|
},
|
|
2299
|
-
cancel() {
|
|
2300
|
-
|
|
2683
|
+
async cancel() {
|
|
2684
|
+
await r.cancel?.()
|
|
2685
|
+
await events.return()
|
|
2301
2686
|
},
|
|
2302
2687
|
})
|
|
2303
|
-
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 } })
|
|
2304
2689
|
}
|
|
2305
2690
|
|
|
2306
2691
|
// ---- the plugin ---------------------------------------------------------------------------
|
|
@@ -2397,4 +2782,4 @@ export async function CursorAuthPlugin() {
|
|
|
2397
2782
|
}
|
|
2398
2783
|
|
|
2399
2784
|
// for tests
|
|
2400
|
-
export const _internal = { onDemandOf,resetH1: () => (h1Until = 0), onH1: () => Date.now() < h1Until, H2, H1, h2Blocked, open, openH1, STEP_WAIT, stepUsage, 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 }
|