@magpie-community/opencode-cursor-auth 0.2.14 → 0.2.15

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
@@ -95,7 +95,15 @@ 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
+ Cursor's count of a step's tool calls includes calls the plugin answers itself:
104
+ a tool the request doesn't have is refused to the model, and Cursor's own
105
+ `get_mcp_tools` is answered with the request's tools. The step's calls are the
106
+ rest.
99
107
 
100
108
  These are retention limits. A missing, expired, restarted, busy, incompatible,
101
109
  or full owner uses the original history reconstruction and stateless decoder
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 (listed > 0 && calls >= listed) {
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 (listed > 0 && calls >= listed) {
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
@@ -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
@@ -530,6 +536,14 @@ class NativeSession {
530
536
  if (this.error || this.transportClosed) invalid("native transport is no longer writable", 410)
531
537
  this.transport.send(message)
532
538
  }
539
+ // reply answers one of Cursor's KV requests once the outbound queue has
540
+ // room. Cursor asks for every history blob at once, hundreds for a long
541
+ // conversation, faster than a slow link takes the replies: the decoder
542
+ // waits for the queue rather than failing the Run (#142).
543
+ async reply(message) {
544
+ await this.transport.room?.(message.length + 5)
545
+ this.send(message)
546
+ }
533
547
  execClose(id) {
534
548
  this.send(this.wire.pb().bytes(5, this.wire.pb().bytes(1, this.wire.pb().varint(1, id))).done())
535
549
  }
@@ -615,6 +629,15 @@ class NativeSession {
615
629
  missingCount() {
616
630
  this.fail({ status: 502, message: "Cursor did not declare its native tool batch", retentionIneligible: !this.resultSubmitted })
617
631
  }
632
+ // A call of the model's step the bridge answered itself: a refused tool (a
633
+ // name it doesn't serve, Cursor's own tools) or get_mcp_tools. Cursor's
634
+ // count is of every call the step made, so it counts toward the batch;
635
+ // never handed to the caller (#142).
636
+ answeredCall() {
637
+ if (this.terminal || this.state === "awaiting-results") return
638
+ this.segment.answered++
639
+ this.sealBatch()
640
+ }
618
641
  listed(count) {
619
642
  if (!Number.isSafeInteger(count) || count < 1) invalid("invalid native tool count", 502)
620
643
  if (this.segment.expected != null && this.segment.expected !== count) invalid("inconsistent native tool count", 502)
@@ -626,8 +649,18 @@ class NativeSession {
626
649
  sealBatch() {
627
650
  const segment = this.segment
628
651
  if (segment.expected == null) return
629
- if (segment.calls.length > segment.expected) invalid("native tool batch exceeded its declared count", 502)
630
- if (segment.calls.length !== segment.expected || segment.closed) return
652
+ const made = segment.calls.length + segment.answered
653
+ if (made > segment.expected) invalid("native tool batch exceeded its declared count", 502)
654
+ if (made !== segment.expected || segment.closed) return
655
+ if (!segment.calls.length) {
656
+ // Every call of the step was answered here: Cursor goes on with the
657
+ // turn by itself, and its next step lists a count of its own.
658
+ segment.expected = null
659
+ segment.answered = 0
660
+ clearTimeout(this.batchTimer)
661
+ this.batchTimer = null
662
+ return
663
+ }
631
664
  clearTimeout(this.batchTimer)
632
665
  this.batchTimer = null
633
666
  this.state = "awaiting-results"
@@ -793,6 +826,8 @@ class NativeSession {
793
826
  case 2: {
794
827
  const c = exec(m.data, this.chatTools, (b) => this.send(b), (id) => this.execClose(id), null, true)
795
828
  if (c) this.acceptCall(c)
829
+ // request_context is Cursor asking for the environment, not a call
830
+ else if (!nativeFields(m.data).some((e) => e.num === 10)) this.answeredCall()
796
831
  break
797
832
  }
798
833
  case 3:
@@ -806,7 +841,7 @@ class NativeSession {
806
841
  const want = field(nativeFields(k.data), 1)?.data.toString("hex") ?? ""
807
842
  const b = this.blobs.get(want)
808
843
  const res = b ? pb().bytes(1, b) : pb().bytes(2, pb().str(1, "blob not found"))
809
- this.send(pb().bytes(3, pb().varint(1, id).bytes(2, res)).done())
844
+ await this.reply(pb().bytes(3, pb().varint(1, id).bytes(2, res)).done())
810
845
  } else if (k.num === 3) {
811
846
  try {
812
847
  const set = nativeFields(k.data)
@@ -815,7 +850,7 @@ class NativeSession {
815
850
  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
851
  throw e
817
852
  }
818
- this.send(pb().bytes(3, pb().varint(1, id).bytes(3, Buffer.alloc(0))).done())
853
+ await this.reply(pb().bytes(3, pb().varint(1, id).bytes(3, Buffer.alloc(0))).done())
819
854
  }
820
855
  }
821
856
  break
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@magpie-community/opencode-cursor-auth",
3
- "version": "0.2.14",
3
+ "version": "0.2.15",
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",