@magpie-community/opencode-cursor-auth 0.2.14 → 0.2.16
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 +18 -2
- package/index.mjs +52 -6
- package/native-session.mjs +82 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -95,7 +95,19 @@ uses the same session/cache key and first user message as `conversation_id`, so
|
|
|
95
95
|
Task subagents sharing their parent's session can run independently. Retention
|
|
96
96
|
waits at most 120 seconds for a result batch and 15 minutes for a Run's total
|
|
97
97
|
lifetime. The pool holds at most 8 owners per account and 32 per process, with
|
|
98
|
-
bounded history, blobs, arguments, and pending writes.
|
|
98
|
+
bounded history, blobs, arguments, and pending writes. When pending writes
|
|
99
|
+
reach their bound (a long history asks for its blobs at once, and a slow link
|
|
100
|
+
drains them slowly), the Run waits for the link to drain, and fails only if it
|
|
101
|
+
doesn't for 30 seconds.
|
|
102
|
+
|
|
103
|
+
A step's calls must all arrive within 30 seconds of each other: Cursor sends
|
|
104
|
+
each one once the model has written it, so a step of many long calls can take
|
|
105
|
+
longer than that in all (#155).
|
|
106
|
+
|
|
107
|
+
Cursor's count of a step's tool calls includes calls the plugin answers itself:
|
|
108
|
+
a tool the request doesn't have is refused to the model, and Cursor's own
|
|
109
|
+
`get_mcp_tools` is answered with the request's tools. The step's calls are the
|
|
110
|
+
rest.
|
|
99
111
|
|
|
100
112
|
These are retention limits. A missing, expired, restarted, busy, incompatible,
|
|
101
113
|
or full owner uses the original history reconstruction and stateless decoder
|
|
@@ -107,7 +119,11 @@ request can rebuild from its supplied history without submitting the result to
|
|
|
107
119
|
the old native exec again.
|
|
108
120
|
|
|
109
121
|
A native tool handoff reports zero usage with `magpie_native.accounting=pending`;
|
|
110
|
-
the final native answer
|
|
122
|
+
the final native answer bills the turn's totals. Its own `prompt_tokens` and
|
|
123
|
+
`completion_tokens` are its last model call's share (the larger of that
|
|
124
|
+
call's prompt estimate and the turn's mean per call), since a client reads
|
|
125
|
+
them as how full its context is; the earlier calls' share is
|
|
126
|
+
`usage.magpie_deferred` (#156). An identical completed retry
|
|
111
127
|
replays the saved response with zero usage. A native final also collects earlier
|
|
112
128
|
stateless debt as `usage.magpie_deferred`, separately from its own prompt total.
|
|
113
129
|
If a started native Run is abandoned after a pending handoff, its dashboard
|
package/index.mjs
CHANGED
|
@@ -1939,6 +1939,15 @@ function nativeTransport(req, close, fail, limits = NATIVE_LIMITS) {
|
|
|
1939
1939
|
const outbound = { bytes: 0, messages: 0 }
|
|
1940
1940
|
const queue = []
|
|
1941
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
|
+
}
|
|
1942
1951
|
const flush = async () => {
|
|
1943
1952
|
if (closed || flushing) return
|
|
1944
1953
|
flushing = true
|
|
@@ -1975,7 +1984,7 @@ function nativeTransport(req, close, fail, limits = NATIVE_LIMITS) {
|
|
|
1975
1984
|
check()
|
|
1976
1985
|
} catch (e) { finish(e) }
|
|
1977
1986
|
})
|
|
1978
|
-
if (!closed) { outbound.bytes -= packet.length; outbound.messages
|
|
1987
|
+
if (!closed) { outbound.bytes -= packet.length; outbound.messages--; wake() }
|
|
1979
1988
|
}
|
|
1980
1989
|
} catch (e) {
|
|
1981
1990
|
if (!closed) fail(e)
|
|
@@ -1993,12 +2002,35 @@ function nativeTransport(req, close, fail, limits = NATIVE_LIMITS) {
|
|
|
1993
2002
|
queue.push(frame(message))
|
|
1994
2003
|
void flush()
|
|
1995
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
|
+
},
|
|
1996
2027
|
close() {
|
|
1997
2028
|
if (closed) return
|
|
1998
2029
|
closed = true
|
|
1999
2030
|
queue.length = 0
|
|
2000
2031
|
outbound.bytes = 0
|
|
2001
2032
|
outbound.messages = 0
|
|
2033
|
+
wake()
|
|
2002
2034
|
rejectWrite?.(new NativeSessionError(410, "native transport was cancelled"))
|
|
2003
2035
|
close()
|
|
2004
2036
|
},
|
|
@@ -2232,9 +2264,22 @@ async function runOnce({ tok, base, id, tools, conv, convID, signal, maxMode, pa
|
|
|
2232
2264
|
// turn ends, or the model has made its tool calls.
|
|
2233
2265
|
async function* decode(stream, { send, blobs, tools, estimate, close, stepUsage, owedUsage }) {
|
|
2234
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
|
|
2235
2270
|
// ended: Cursor said what the turn used
|
|
2236
2271
|
let ended = false
|
|
2237
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
|
+
}
|
|
2238
2283
|
let said = 0
|
|
2239
2284
|
// wrote: the reply's text alone; a turn of thinking only says nothing
|
|
2240
2285
|
let wrote = 0
|
|
@@ -2342,9 +2387,9 @@ async function* decode(stream, { send, blobs, tools, estimate, close, stepUsage,
|
|
|
2342
2387
|
return
|
|
2343
2388
|
}
|
|
2344
2389
|
break
|
|
2345
|
-
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
|
|
2346
2391
|
listed = pbNum(uf, 1)
|
|
2347
|
-
if (
|
|
2392
|
+
if (stepDone()) {
|
|
2348
2393
|
yield await settled()
|
|
2349
2394
|
return
|
|
2350
2395
|
}
|
|
@@ -2360,8 +2405,8 @@ async function* decode(stream, { send, blobs, tools, estimate, close, stepUsage,
|
|
|
2360
2405
|
if (call) {
|
|
2361
2406
|
said += call.args.length
|
|
2362
2407
|
yield { tool: { index: calls++, ...call } }
|
|
2363
|
-
}
|
|
2364
|
-
if (
|
|
2408
|
+
} else if (!fields(m.data).some((e) => e.num === 10)) answered++
|
|
2409
|
+
if (stepDone()) {
|
|
2365
2410
|
// every call made: the caller runs them
|
|
2366
2411
|
yield await settled()
|
|
2367
2412
|
return
|
|
@@ -2547,7 +2592,8 @@ async function answerOf(auth, chat, signal, session = "") {
|
|
|
2547
2592
|
const convID = conversationID(chat, session)
|
|
2548
2593
|
const prepared = nativeSessions.prepare(tok, chat, convID, tools)
|
|
2549
2594
|
let r
|
|
2550
|
-
|
|
2595
|
+
// the continuation's own prompt, which its final step reports (#156)
|
|
2596
|
+
if (prepared.owner) r = prepared.owner.resume({ ...prepared, estimate: conversation(chat, tools).estimate }, signal)
|
|
2551
2597
|
const model = String(chat.model ?? "")
|
|
2552
2598
|
if (!r) {
|
|
2553
2599
|
let raw = []
|
package/native-session.mjs
CHANGED
|
@@ -16,6 +16,9 @@ export const NATIVE_LIMITS = {
|
|
|
16
16
|
argumentDepth: 64,
|
|
17
17
|
outboundBytes: 16 << 20,
|
|
18
18
|
outboundMessages: 256,
|
|
19
|
+
// How long a KV reply waits for room in the outbound queue (Cursor's own
|
|
20
|
+
// flow control taking the writes before it) before the Run fails.
|
|
21
|
+
writeWait: 30_000,
|
|
19
22
|
perAccount: 8,
|
|
20
23
|
perProcess: 32,
|
|
21
24
|
}
|
|
@@ -270,6 +273,9 @@ class Segment {
|
|
|
270
273
|
this.prefixDigest = historyDigest(chat.messages)
|
|
271
274
|
this.events = []
|
|
272
275
|
this.calls = []
|
|
276
|
+
// answered: the step's execs the bridge answered itself (a refusal, or
|
|
277
|
+
// the MCP tool list). Cursor's tool_requests_listed counts them too.
|
|
278
|
+
this.answered = 0
|
|
273
279
|
this.content = ""
|
|
274
280
|
this.reasoning = ""
|
|
275
281
|
this.expected = null
|
|
@@ -448,6 +454,10 @@ class NativeSession {
|
|
|
448
454
|
this.retainedBytes = 0
|
|
449
455
|
this.sequence = 0
|
|
450
456
|
this.segments = 0
|
|
457
|
+
// the model calls of the turn so far, and what was said before the
|
|
458
|
+
// latest one began: the last step's own share of the turn (#156)
|
|
459
|
+
this.steps = 1
|
|
460
|
+
this.stepSaid = 0
|
|
451
461
|
this.said = 0
|
|
452
462
|
this.wrote = 0
|
|
453
463
|
this.terminal = false
|
|
@@ -509,6 +519,7 @@ class NativeSession {
|
|
|
509
519
|
}
|
|
510
520
|
this.transport = transport
|
|
511
521
|
this.estimate = estimate
|
|
522
|
+
this.stepEstimate = estimate
|
|
512
523
|
this.blobs = blobs
|
|
513
524
|
this.knownHashes = new Set(blobs.keys())
|
|
514
525
|
this.blobSize = [...blobs.values()].reduce((n, b) => n + b.length, 0)
|
|
@@ -530,6 +541,14 @@ class NativeSession {
|
|
|
530
541
|
if (this.error || this.transportClosed) invalid("native transport is no longer writable", 410)
|
|
531
542
|
this.transport.send(message)
|
|
532
543
|
}
|
|
544
|
+
// reply answers one of Cursor's KV requests once the outbound queue has
|
|
545
|
+
// room. Cursor asks for every history blob at once, hundreds for a long
|
|
546
|
+
// conversation, faster than a slow link takes the replies: the decoder
|
|
547
|
+
// waits for the queue rather than failing the Run (#142).
|
|
548
|
+
async reply(message) {
|
|
549
|
+
await this.transport.room?.(message.length + 5)
|
|
550
|
+
this.send(message)
|
|
551
|
+
}
|
|
533
552
|
execClose(id) {
|
|
534
553
|
this.send(this.wire.pb().bytes(5, this.wire.pb().bytes(1, this.wire.pb().varint(1, id))).done())
|
|
535
554
|
}
|
|
@@ -557,6 +576,8 @@ class NativeSession {
|
|
|
557
576
|
this.state = "submitting"
|
|
558
577
|
clearTimeout(this.resultTimer)
|
|
559
578
|
clearTimeout(this.batchTimer)
|
|
579
|
+
this.batchTimer = null
|
|
580
|
+
this.nextStep(prepared.estimate)
|
|
560
581
|
try {
|
|
561
582
|
for (let i = 0; i < prepared.results.length; i++) {
|
|
562
583
|
const call = batch.calls.find((c) => c.id === prepared.results[i].tool_call_id)
|
|
@@ -606,8 +627,20 @@ class NativeSession {
|
|
|
606
627
|
this.waitForBatch()
|
|
607
628
|
this.sealBatch()
|
|
608
629
|
}
|
|
630
|
+
// nextStep is Cursor's next model call in the same turn: after the
|
|
631
|
+
// caller's results, or after a step whose calls were all answered here.
|
|
632
|
+
nextStep(estimate) {
|
|
633
|
+
this.steps++
|
|
634
|
+
this.stepSaid = this.said
|
|
635
|
+
if (estimate != null) this.stepEstimate = estimate
|
|
636
|
+
}
|
|
637
|
+
// waitForBatch gives the step batchWait from its latest call, answered
|
|
638
|
+
// call or count, not from its first. Cursor sends each call once the model
|
|
639
|
+
// has written its arguments, so a step of many long calls arrives over
|
|
640
|
+
// longer than batchWait while never pausing that long (#155).
|
|
609
641
|
waitForBatch() {
|
|
610
|
-
this.batchTimer
|
|
642
|
+
clearTimeout(this.batchTimer)
|
|
643
|
+
this.batchTimer = later(() => {
|
|
611
644
|
if (this.segment.expected == null && this.segment.calls.length && !this.resultSubmitted) this.missingCount()
|
|
612
645
|
else this.fail({ status: 502, message: "Cursor did not complete its declared native tool batch" })
|
|
613
646
|
}, this.limits.batchWait)
|
|
@@ -615,6 +648,16 @@ class NativeSession {
|
|
|
615
648
|
missingCount() {
|
|
616
649
|
this.fail({ status: 502, message: "Cursor did not declare its native tool batch", retentionIneligible: !this.resultSubmitted })
|
|
617
650
|
}
|
|
651
|
+
// A call of the model's step the bridge answered itself: a refused tool (a
|
|
652
|
+
// name it doesn't serve, Cursor's own tools) or get_mcp_tools. Cursor's
|
|
653
|
+
// count is of every call the step made, so it counts toward the batch;
|
|
654
|
+
// never handed to the caller (#142).
|
|
655
|
+
answeredCall() {
|
|
656
|
+
if (this.terminal || this.state === "awaiting-results") return
|
|
657
|
+
this.segment.answered++
|
|
658
|
+
if (this.batchTimer) this.waitForBatch()
|
|
659
|
+
this.sealBatch()
|
|
660
|
+
}
|
|
618
661
|
listed(count) {
|
|
619
662
|
if (!Number.isSafeInteger(count) || count < 1) invalid("invalid native tool count", 502)
|
|
620
663
|
if (this.segment.expected != null && this.segment.expected !== count) invalid("inconsistent native tool count", 502)
|
|
@@ -626,8 +669,19 @@ class NativeSession {
|
|
|
626
669
|
sealBatch() {
|
|
627
670
|
const segment = this.segment
|
|
628
671
|
if (segment.expected == null) return
|
|
629
|
-
|
|
630
|
-
if (
|
|
672
|
+
const made = segment.calls.length + segment.answered
|
|
673
|
+
if (made > segment.expected) invalid("native tool batch exceeded its declared count", 502)
|
|
674
|
+
if (made !== segment.expected || segment.closed) return
|
|
675
|
+
if (!segment.calls.length) {
|
|
676
|
+
// Every call of the step was answered here: Cursor goes on with the
|
|
677
|
+
// turn by itself, and its next step lists a count of its own.
|
|
678
|
+
segment.expected = null
|
|
679
|
+
segment.answered = 0
|
|
680
|
+
clearTimeout(this.batchTimer)
|
|
681
|
+
this.batchTimer = null
|
|
682
|
+
this.nextStep()
|
|
683
|
+
return
|
|
684
|
+
}
|
|
631
685
|
clearTimeout(this.batchTimer)
|
|
632
686
|
this.batchTimer = null
|
|
633
687
|
this.state = "awaiting-results"
|
|
@@ -729,6 +783,24 @@ class NativeSession {
|
|
|
729
783
|
const u = usageOf(this.usageFields ?? [])
|
|
730
784
|
if (!measuredInput) u.input = this.estimate
|
|
731
785
|
if (!measuredOutput) u.output = Math.floor((this.said + 3) / 4)
|
|
786
|
+
// A turn of several model calls: TurnEndedUpdate counts them all, but a
|
|
787
|
+
// client reads the last response's prompt as how full its context is
|
|
788
|
+
// (Codex compacted a 61K context at 1.1M, #156). The last call keeps its
|
|
789
|
+
// own share, the larger of its prompt estimate and the turn's mean per
|
|
790
|
+
// call; the rest is billed as magpie_deferred.
|
|
791
|
+
let earlier = null
|
|
792
|
+
if (this.steps > 1) {
|
|
793
|
+
const whole = u.input + u.cacheRead + u.cacheWrite
|
|
794
|
+
const share = (total, own) => total > 0 ? Math.min(1, Math.max(own, total / this.steps) / total) : 0
|
|
795
|
+
const fi = share(whole, measuredInput ? this.stepEstimate ?? 0 : whole)
|
|
796
|
+
const fo = share(u.output, Math.floor((this.said - this.stepSaid + 3) / 4))
|
|
797
|
+
const own = {
|
|
798
|
+
input: Math.round(u.input * fi), cacheRead: Math.round(u.cacheRead * fi), cacheWrite: Math.round(u.cacheWrite * fi),
|
|
799
|
+
output: Math.round(u.output * fo), reasoning: Math.round(u.reasoning * fo),
|
|
800
|
+
}
|
|
801
|
+
earlier = { input: u.input - own.input, cacheRead: u.cacheRead - own.cacheRead, cacheWrite: u.cacheWrite - own.cacheWrite, output: u.output - own.output }
|
|
802
|
+
Object.assign(u, own)
|
|
803
|
+
}
|
|
732
804
|
const prompt = u.input + u.cacheRead + u.cacheWrite
|
|
733
805
|
this.finalUsage = {
|
|
734
806
|
prompt_tokens: prompt, completion_tokens: u.output, total_tokens: prompt + u.output,
|
|
@@ -738,8 +810,9 @@ class NativeSession {
|
|
|
738
810
|
}
|
|
739
811
|
this.state = "finalizing"
|
|
740
812
|
this.closeTransport()
|
|
741
|
-
const finish = (
|
|
813
|
+
const finish = (settled) => {
|
|
742
814
|
if (this.error) return
|
|
815
|
+
const more = earlier && settled ? Object.fromEntries(Object.keys(earlier).map((k) => [k, earlier[k] + settled[k]])) : earlier ?? settled
|
|
743
816
|
const deferred = more && more.input + more.output + more.cacheRead + more.cacheWrite > 0
|
|
744
817
|
this.segment.finish({ stop: "stop", usage: {
|
|
745
818
|
...this.finalUsage,
|
|
@@ -783,6 +856,7 @@ class NativeSession {
|
|
|
783
856
|
this.said += text.length
|
|
784
857
|
if (u.num === 1) this.wrote += text.length
|
|
785
858
|
this.segment.emit(u.num === 1 ? { text } : { reasoning: text })
|
|
859
|
+
if (this.batchTimer) this.waitForBatch()
|
|
786
860
|
}
|
|
787
861
|
} else if (u.num === 14) {
|
|
788
862
|
if (!this.segment.content && !this.segment.calls.length) invalid("an empty reply (Cursor ended the turn without text or a tool call)", 502)
|
|
@@ -793,6 +867,8 @@ class NativeSession {
|
|
|
793
867
|
case 2: {
|
|
794
868
|
const c = exec(m.data, this.chatTools, (b) => this.send(b), (id) => this.execClose(id), null, true)
|
|
795
869
|
if (c) this.acceptCall(c)
|
|
870
|
+
// request_context is Cursor asking for the environment, not a call
|
|
871
|
+
else if (!nativeFields(m.data).some((e) => e.num === 10)) this.answeredCall()
|
|
796
872
|
break
|
|
797
873
|
}
|
|
798
874
|
case 3:
|
|
@@ -806,7 +882,7 @@ class NativeSession {
|
|
|
806
882
|
const want = field(nativeFields(k.data), 1)?.data.toString("hex") ?? ""
|
|
807
883
|
const b = this.blobs.get(want)
|
|
808
884
|
const res = b ? pb().bytes(1, b) : pb().bytes(2, pb().str(1, "blob not found"))
|
|
809
|
-
this.
|
|
885
|
+
await this.reply(pb().bytes(3, pb().varint(1, id).bytes(2, res)).done())
|
|
810
886
|
} else if (k.num === 3) {
|
|
811
887
|
try {
|
|
812
888
|
const set = nativeFields(k.data)
|
|
@@ -815,7 +891,7 @@ class NativeSession {
|
|
|
815
891
|
this.send(pb().bytes(3, pb().varint(1, id).bytes(3, pb().bytes(1, pb().str(1, "native blob could not be retained")))).done())
|
|
816
892
|
throw e
|
|
817
893
|
}
|
|
818
|
-
this.
|
|
894
|
+
await this.reply(pb().bytes(3, pb().varint(1, id).bytes(3, Buffer.alloc(0))).done())
|
|
819
895
|
}
|
|
820
896
|
}
|
|
821
897
|
break
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@magpie-community/opencode-cursor-auth",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.16",
|
|
4
4
|
"description": "Cursor subscriptions (Pro, Pro+, Ultra, Teams) in OpenCode and magpie, through the API cursor-agent talks to",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./index.mjs",
|