@magpie-community/opencode-cursor-auth 0.2.35 → 0.2.37

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
@@ -101,9 +101,12 @@ Tool results can continue a retained Run in the same plugin process. The owner
101
101
  uses the same session/cache key and first user message as `conversation_id`, so
102
102
  Task subagents sharing their parent's session can run independently. Retention
103
103
  waits at most 120 seconds for a result batch and 15 minutes for a Run's total
104
- lifetime. A continuation arriving in the last minute of that lifetime rebuilds
105
- from supplied history before submitting results to the old Run. The absolute
106
- deadline still applies to an active segment; it is never extended by activity.
104
+ lifetime. A continuation arriving with less than 10 minutes of that lifetime
105
+ left rebuilds from supplied history on a fresh Run before submitting results
106
+ to the old one, since one step can be a long answer. The absolute deadline
107
+ still applies to an active segment; it is never extended by activity. A Run
108
+ cut by it mid-answer ends with `native Run lifetime expired: the Run timed
109
+ out; retry your request`, words agents ask again on.
107
110
  The pool holds at most 8 owners per account and 32 per process, with
108
111
  bounded history, blobs, arguments, and pending writes. When pending writes
109
112
  reach their bound (a long history asks for its blobs at once, and a slow link
package/index.mjs CHANGED
@@ -2853,7 +2853,7 @@ async function runOnce({ tok, base, id, tools, conv, convID, signal, maxMode, pa
2853
2853
 
2854
2854
  // decode follows the server's messages into the answer's pieces until the
2855
2855
  // turn ends, or the model has made its tool calls.
2856
- async function* decode(stream, { send, blobs, tools, estimate, close, stepUsage, owedUsage, note }) {
2856
+ async function* decode(stream, { send, blobs, tools, estimate, close, stepUsage, owedUsage, note, countWait = NATIVE_LIMITS.countWait }) {
2857
2857
  let calls = 0
2858
2858
  // answered: the step's calls answered here (a refusal, the MCP tool
2859
2859
  // list), which Cursor's count of the step's calls takes in too (#142)
@@ -2937,8 +2937,48 @@ async function* decode(stream, { send, blobs, tools, estimate, close, stepUsage,
2937
2937
  return finish(owedUsage ? await owedUsage(calls === 0).catch(() => null) : null)
2938
2938
  }
2939
2939
  const closeExec = (id) => send(pb().bytes(5, pb().bytes(1, pb().varint(1, id))).done())
2940
+ // A step whose calls came and whose count never does: Cursor sent the
2941
+ // calls and then only heartbeats (plugins#550, the fifth trace: two
2942
+ // calls, no count, 240 s of heartbeats until quiet). Once countWait has
2943
+ // passed since Cursor's last frame that wasn't a heartbeat, with no call
2944
+ // still being written, the calls that came are the step's. Heartbeats
2945
+ // never move the deadline: they are all such a Run sends.
2946
+ const it = (stream[Symbol.asyncIterator] ?? stream[Symbol.iterator]).call(stream)
2947
+ let heardAt = Date.now()
2948
+ const read = async () => {
2949
+ const next = Promise.resolve(it.next())
2950
+ if (!calls || listed || step.writing) return next
2951
+ next.catch(() => {}) // a read left behind by the cut fails unheard
2952
+ let timer
2953
+ const cut = new Promise((r) => {
2954
+ timer = setTimeout(() => r({ cut: true }), Math.max(0, heardAt + countWait - Date.now()))
2955
+ timer.unref?.()
2956
+ })
2957
+ try {
2958
+ return await Promise.race([next, cut])
2959
+ } finally {
2960
+ clearTimeout(timer)
2961
+ }
2962
+ }
2963
+ const beat = (data) => {
2964
+ try {
2965
+ const ms = fields(data)
2966
+ return ms.length > 0 && ms.every((m) => m.num === 1 && fields(m.data).every((u) => u.num === 13))
2967
+ } catch {
2968
+ return false
2969
+ }
2970
+ }
2940
2971
  try {
2941
- for await (const f of stream) {
2972
+ for (;;) {
2973
+ const r = await read()
2974
+ if (r.cut) {
2975
+ note?.("no count")
2976
+ yield await settled()
2977
+ return
2978
+ }
2979
+ if (r.done) break
2980
+ const f = r.value
2981
+ if (f.end || !beat(f.data)) heardAt = Date.now()
2942
2982
  if (f.end) {
2943
2983
  const e = failure(200, f.data)
2944
2984
  if (e.status < 200 || e.status > 299) yield { error: e }
@@ -3072,6 +3112,9 @@ async function* decode(stream, { send, blobs, tools, estimate, close, stepUsage,
3072
3112
  return
3073
3113
  }
3074
3114
  yield { error: { status: 502, message: "the reply broke off: " + (e?.message ?? e) } }
3115
+ } finally {
3116
+ // not waited for: after a cut, the read left behind ends with the Run
3117
+ Promise.resolve(it.return?.()).catch(() => {})
3075
3118
  }
3076
3119
  }
3077
3120
 
@@ -8,7 +8,20 @@ import { join } from "node:path"
8
8
  export const NATIVE_LIMITS = {
9
9
  resultWait: 120_000,
10
10
  lifetime: 15 * 60_000,
11
+ // How much of the lifetime a retained Run must have left to take a tool
12
+ // batch's results and go on; with less, the step starts on a fresh Run
13
+ // from the supplied history. One step can be a long answer: a Pi turn on
14
+ // grok-4.7 went on with 494 s left and was still answering at the
15
+ // deadline (plugins#616, yetone/magpie#4419). At most two thirds of the
16
+ // lifetime, so a short one set in a test still has room to continue.
17
+ continueReserve: 10 * 60_000,
11
18
  batchWait: 30_000,
19
+ // How long a stateless step whose calls came waits for Cursor's count of
20
+ // them (update 27) with nothing but heartbeats before it hands the calls
21
+ // that came to the caller (plugins#550). A count comes within ms of the
22
+ // step's last call; a Run that sent calls and then only heartbeats has
23
+ // stopped, and the caller's next request goes on from the calls' results.
24
+ countWait: 60_000,
12
25
  drain: 6000,
13
26
  replayWait: 120_000,
14
27
  blobBytes: 64 << 20,
@@ -511,6 +524,14 @@ export class StepCalls {
511
524
  get completed() {
512
525
  return this.byCursor.size
513
526
  }
527
+ // writing is how many calls the step wrote (7) that are neither started
528
+ // on the client, sent as an exec, nor completed by Cursor: a call of
529
+ // Cursor's own still running, or one still to be sent
530
+ get writing() {
531
+ let n = 0
532
+ for (const id of this.partial) if (!this.started.has(id) && !this.execs.has(id) && !this.byCursor.has(id)) n++
533
+ return n
534
+ }
514
535
  // nextStep starts the next step's count
515
536
  nextStep() {
516
537
  this.partial.clear()
@@ -714,6 +735,13 @@ class Segment {
714
735
  }
715
736
  }
716
737
 
738
+ // What a Run cut at its absolute deadline says. It most often lands
739
+ // mid-answer, the 200 long sent, so the agent reads only these words; the
740
+ // conversation is not lost (asked again, the history goes on a fresh Run),
741
+ // and they say so in words agents retry on: "timed out" (Pi, magpie's
742
+ // passingSaid) and "retry your request" (plugins#616, yetone/magpie#4419).
743
+ export const LIFETIME_EXPIRED = "native Run lifetime expired: the Run timed out; retry your request"
744
+
717
745
  export class NativeSessionPool {
718
746
  constructor(limits = NATIVE_LIMITS) {
719
747
  this.limits = limits
@@ -755,7 +783,7 @@ export class NativeSessionPool {
755
783
  // A retained Run's absolute timer spans all tool roundtrips. Rebuild
756
784
  // near its deadline before writing this batch: otherwise a new Chat
757
785
  // segment can lose its transport after already exposing output.
758
- const reserve = Math.min(60_000, owner.limits.lifetime / 10)
786
+ const reserve = Math.min(owner.limits.continueReserve, owner.limits.lifetime * 2 / 3)
759
787
  if (owner.expiresAt - Date.now() <= reserve) {
760
788
  owner.fail({ status: 410, message: "native Run retired before lifetime deadline" }, "expired")
761
789
  return fallback("owner near lifetime deadline")
@@ -853,7 +881,7 @@ class NativeSession {
853
881
  expireIn(ms) {
854
882
  clearTimeout(this.lifetimeTimer)
855
883
  this.expiresAt = Date.now() + ms
856
- this.lifetimeTimer = later(() => this.fail({ status: 410, message: "native Run lifetime expired" }, "expired"), ms)
884
+ this.lifetimeTimer = later(() => this.fail({ status: 410, message: LIFETIME_EXPIRED }, "expired"), ms)
857
885
  }
858
886
  retain(n) {
859
887
  if (this.retainedBytes + n > this.limits.outputBytes) invalid("native output/history retention limit reached", 503)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@magpie-community/opencode-cursor-auth",
3
- "version": "0.2.35",
3
+ "version": "0.2.37",
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",