@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 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. The pool holds at most 8 owners per account and 32 per process, with
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 reports the turn's totals. An identical completed retry
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
- if (prepared.owner) r = prepared.owner.resume(prepared, signal)
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 = []
@@ -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.lifetimeTimer = later(() => this.fail({ status: 410, message: "native Run lifetime expired" }, "expired"), this.limits.lifetime)
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 ??= later(() => {
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 = (more) => {
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.15",
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",