@magpie-community/opencode-cursor-auth 0.2.15 → 0.2.17
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 +13 -2
- package/index.mjs +2 -1
- package/native-session.mjs +59 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -94,12 +94,19 @@ Tool results can continue a retained Run in the same plugin process. The owner
|
|
|
94
94
|
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
|
-
lifetime.
|
|
97
|
+
lifetime. A continuation arriving in the last minute of that lifetime rebuilds
|
|
98
|
+
from supplied history before submitting results to the old Run. The absolute
|
|
99
|
+
deadline still applies to an active segment; it is never extended by activity.
|
|
100
|
+
The pool holds at most 8 owners per account and 32 per process, with
|
|
98
101
|
bounded history, blobs, arguments, and pending writes. When pending writes
|
|
99
102
|
reach their bound (a long history asks for its blobs at once, and a slow link
|
|
100
103
|
drains them slowly), the Run waits for the link to drain, and fails only if it
|
|
101
104
|
doesn't for 30 seconds.
|
|
102
105
|
|
|
106
|
+
A step's calls must all arrive within 30 seconds of each other: Cursor sends
|
|
107
|
+
each one once the model has written it, so a step of many long calls can take
|
|
108
|
+
longer than that in all (#155).
|
|
109
|
+
|
|
103
110
|
Cursor's count of a step's tool calls includes calls the plugin answers itself:
|
|
104
111
|
a tool the request doesn't have is refused to the model, and Cursor's own
|
|
105
112
|
`get_mcp_tools` is answered with the request's tools. The step's calls are the
|
|
@@ -115,7 +122,11 @@ request can rebuild from its supplied history without submitting the result to
|
|
|
115
122
|
the old native exec again.
|
|
116
123
|
|
|
117
124
|
A native tool handoff reports zero usage with `magpie_native.accounting=pending`;
|
|
118
|
-
the final native answer
|
|
125
|
+
the final native answer bills the turn's totals. Its own `prompt_tokens` and
|
|
126
|
+
`completion_tokens` are its last model call's share (the larger of that
|
|
127
|
+
call's prompt estimate and the turn's mean per call), since a client reads
|
|
128
|
+
them as how full its context is; the earlier calls' share is
|
|
129
|
+
`usage.magpie_deferred` (#156). An identical completed retry
|
|
119
130
|
replays the saved response with zero usage. A native final also collects earlier
|
|
120
131
|
stateless debt as `usage.magpie_deferred`, separately from its own prompt total.
|
|
121
132
|
If a started native Run is abandoned after a pending handoff, its dashboard
|
package/index.mjs
CHANGED
|
@@ -2592,7 +2592,8 @@ async function answerOf(auth, chat, signal, session = "") {
|
|
|
2592
2592
|
const convID = conversationID(chat, session)
|
|
2593
2593
|
const prepared = nativeSessions.prepare(tok, chat, convID, tools)
|
|
2594
2594
|
let r
|
|
2595
|
-
|
|
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)
|
|
2596
2597
|
const model = String(chat.model ?? "")
|
|
2597
2598
|
if (!r) {
|
|
2598
2599
|
let raw = []
|
package/native-session.mjs
CHANGED
|
@@ -381,6 +381,14 @@ export class NativeSessionPool {
|
|
|
381
381
|
return { owner, batch, replay: true, segment: batch.next }
|
|
382
382
|
}
|
|
383
383
|
if (owner.state !== "awaiting-results" || owner.segment !== batch || !batch.clean) return fallback("owner busy")
|
|
384
|
+
// A retained Run's absolute timer spans all tool roundtrips. Rebuild
|
|
385
|
+
// near its deadline before writing this batch: otherwise a new Chat
|
|
386
|
+
// segment can lose its transport after already exposing output.
|
|
387
|
+
const reserve = Math.min(60_000, owner.limits.lifetime / 10)
|
|
388
|
+
if (owner.expiresAt - Date.now() <= reserve) {
|
|
389
|
+
owner.fail({ status: 410, message: "native Run retired before lifetime deadline" }, "expired")
|
|
390
|
+
return fallback("owner near lifetime deadline")
|
|
391
|
+
}
|
|
384
392
|
return { owner, batch, results, encoded, digest, chat }
|
|
385
393
|
} catch (e) {
|
|
386
394
|
if (!(e instanceof NativeSessionError)) throw e
|
|
@@ -454,12 +462,23 @@ class NativeSession {
|
|
|
454
462
|
this.retainedBytes = 0
|
|
455
463
|
this.sequence = 0
|
|
456
464
|
this.segments = 0
|
|
465
|
+
// the model calls of the turn so far, and what was said before the
|
|
466
|
+
// latest one began: the last step's own share of the turn (#156)
|
|
467
|
+
this.steps = 1
|
|
468
|
+
this.stepSaid = 0
|
|
457
469
|
this.said = 0
|
|
458
470
|
this.wrote = 0
|
|
459
471
|
this.terminal = false
|
|
460
472
|
this.segment = new Segment(this, prepared.chat)
|
|
461
473
|
this.retain(bytes(prepared.chat))
|
|
462
|
-
this.
|
|
474
|
+
this.expireIn(this.limits.lifetime)
|
|
475
|
+
}
|
|
476
|
+
// expireIn sets the Run's absolute deadline ms from now: from its start,
|
|
477
|
+
// the whole lifetime; a test brings it in after the first answer
|
|
478
|
+
expireIn(ms) {
|
|
479
|
+
clearTimeout(this.lifetimeTimer)
|
|
480
|
+
this.expiresAt = Date.now() + ms
|
|
481
|
+
this.lifetimeTimer = later(() => this.fail({ status: 410, message: "native Run lifetime expired" }, "expired"), ms)
|
|
463
482
|
}
|
|
464
483
|
retain(n) {
|
|
465
484
|
if (this.retainedBytes + n > this.limits.outputBytes) invalid("native output/history retention limit reached", 503)
|
|
@@ -515,6 +534,7 @@ class NativeSession {
|
|
|
515
534
|
}
|
|
516
535
|
this.transport = transport
|
|
517
536
|
this.estimate = estimate
|
|
537
|
+
this.stepEstimate = estimate
|
|
518
538
|
this.blobs = blobs
|
|
519
539
|
this.knownHashes = new Set(blobs.keys())
|
|
520
540
|
this.blobSize = [...blobs.values()].reduce((n, b) => n + b.length, 0)
|
|
@@ -571,6 +591,8 @@ class NativeSession {
|
|
|
571
591
|
this.state = "submitting"
|
|
572
592
|
clearTimeout(this.resultTimer)
|
|
573
593
|
clearTimeout(this.batchTimer)
|
|
594
|
+
this.batchTimer = null
|
|
595
|
+
this.nextStep(prepared.estimate)
|
|
574
596
|
try {
|
|
575
597
|
for (let i = 0; i < prepared.results.length; i++) {
|
|
576
598
|
const call = batch.calls.find((c) => c.id === prepared.results[i].tool_call_id)
|
|
@@ -620,8 +642,20 @@ class NativeSession {
|
|
|
620
642
|
this.waitForBatch()
|
|
621
643
|
this.sealBatch()
|
|
622
644
|
}
|
|
645
|
+
// nextStep is Cursor's next model call in the same turn: after the
|
|
646
|
+
// caller's results, or after a step whose calls were all answered here.
|
|
647
|
+
nextStep(estimate) {
|
|
648
|
+
this.steps++
|
|
649
|
+
this.stepSaid = this.said
|
|
650
|
+
if (estimate != null) this.stepEstimate = estimate
|
|
651
|
+
}
|
|
652
|
+
// waitForBatch gives the step batchWait from its latest call, answered
|
|
653
|
+
// call or count, not from its first. Cursor sends each call once the model
|
|
654
|
+
// has written its arguments, so a step of many long calls arrives over
|
|
655
|
+
// longer than batchWait while never pausing that long (#155).
|
|
623
656
|
waitForBatch() {
|
|
624
|
-
this.batchTimer
|
|
657
|
+
clearTimeout(this.batchTimer)
|
|
658
|
+
this.batchTimer = later(() => {
|
|
625
659
|
if (this.segment.expected == null && this.segment.calls.length && !this.resultSubmitted) this.missingCount()
|
|
626
660
|
else this.fail({ status: 502, message: "Cursor did not complete its declared native tool batch" })
|
|
627
661
|
}, this.limits.batchWait)
|
|
@@ -636,6 +670,7 @@ class NativeSession {
|
|
|
636
670
|
answeredCall() {
|
|
637
671
|
if (this.terminal || this.state === "awaiting-results") return
|
|
638
672
|
this.segment.answered++
|
|
673
|
+
if (this.batchTimer) this.waitForBatch()
|
|
639
674
|
this.sealBatch()
|
|
640
675
|
}
|
|
641
676
|
listed(count) {
|
|
@@ -659,6 +694,7 @@ class NativeSession {
|
|
|
659
694
|
segment.answered = 0
|
|
660
695
|
clearTimeout(this.batchTimer)
|
|
661
696
|
this.batchTimer = null
|
|
697
|
+
this.nextStep()
|
|
662
698
|
return
|
|
663
699
|
}
|
|
664
700
|
clearTimeout(this.batchTimer)
|
|
@@ -762,6 +798,24 @@ class NativeSession {
|
|
|
762
798
|
const u = usageOf(this.usageFields ?? [])
|
|
763
799
|
if (!measuredInput) u.input = this.estimate
|
|
764
800
|
if (!measuredOutput) u.output = Math.floor((this.said + 3) / 4)
|
|
801
|
+
// A turn of several model calls: TurnEndedUpdate counts them all, but a
|
|
802
|
+
// client reads the last response's prompt as how full its context is
|
|
803
|
+
// (Codex compacted a 61K context at 1.1M, #156). The last call keeps its
|
|
804
|
+
// own share, the larger of its prompt estimate and the turn's mean per
|
|
805
|
+
// call; the rest is billed as magpie_deferred.
|
|
806
|
+
let earlier = null
|
|
807
|
+
if (this.steps > 1) {
|
|
808
|
+
const whole = u.input + u.cacheRead + u.cacheWrite
|
|
809
|
+
const share = (total, own) => total > 0 ? Math.min(1, Math.max(own, total / this.steps) / total) : 0
|
|
810
|
+
const fi = share(whole, measuredInput ? this.stepEstimate ?? 0 : whole)
|
|
811
|
+
const fo = share(u.output, Math.floor((this.said - this.stepSaid + 3) / 4))
|
|
812
|
+
const own = {
|
|
813
|
+
input: Math.round(u.input * fi), cacheRead: Math.round(u.cacheRead * fi), cacheWrite: Math.round(u.cacheWrite * fi),
|
|
814
|
+
output: Math.round(u.output * fo), reasoning: Math.round(u.reasoning * fo),
|
|
815
|
+
}
|
|
816
|
+
earlier = { input: u.input - own.input, cacheRead: u.cacheRead - own.cacheRead, cacheWrite: u.cacheWrite - own.cacheWrite, output: u.output - own.output }
|
|
817
|
+
Object.assign(u, own)
|
|
818
|
+
}
|
|
765
819
|
const prompt = u.input + u.cacheRead + u.cacheWrite
|
|
766
820
|
this.finalUsage = {
|
|
767
821
|
prompt_tokens: prompt, completion_tokens: u.output, total_tokens: prompt + u.output,
|
|
@@ -771,8 +825,9 @@ class NativeSession {
|
|
|
771
825
|
}
|
|
772
826
|
this.state = "finalizing"
|
|
773
827
|
this.closeTransport()
|
|
774
|
-
const finish = (
|
|
828
|
+
const finish = (settled) => {
|
|
775
829
|
if (this.error) return
|
|
830
|
+
const more = earlier && settled ? Object.fromEntries(Object.keys(earlier).map((k) => [k, earlier[k] + settled[k]])) : earlier ?? settled
|
|
776
831
|
const deferred = more && more.input + more.output + more.cacheRead + more.cacheWrite > 0
|
|
777
832
|
this.segment.finish({ stop: "stop", usage: {
|
|
778
833
|
...this.finalUsage,
|
|
@@ -816,6 +871,7 @@ class NativeSession {
|
|
|
816
871
|
this.said += text.length
|
|
817
872
|
if (u.num === 1) this.wrote += text.length
|
|
818
873
|
this.segment.emit(u.num === 1 ? { text } : { reasoning: text })
|
|
874
|
+
if (this.batchTimer) this.waitForBatch()
|
|
819
875
|
}
|
|
820
876
|
} else if (u.num === 14) {
|
|
821
877
|
if (!this.segment.content && !this.segment.calls.length) invalid("an empty reply (Cursor ended the turn without text or a tool call)", 502)
|
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.17",
|
|
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",
|