@magpie-community/opencode-cursor-auth 0.2.34 → 0.2.36
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 +9 -3
- package/index.mjs +85 -10
- package/native-session.mjs +133 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -123,8 +123,11 @@ caller's tool results is bounded by the 120 seconds above instead.
|
|
|
123
123
|
|
|
124
124
|
Cursor's count of a step's tool calls includes calls the plugin answers itself:
|
|
125
125
|
a tool the request doesn't have is refused to the model, and Cursor's own
|
|
126
|
-
`get_mcp_tools` is answered with the request's tools.
|
|
127
|
-
|
|
126
|
+
`get_mcp_tools` is answered with the request's tools. It also includes calls
|
|
127
|
+
Cursor completes by itself, which it writes and completes in the step but never
|
|
128
|
+
starts on the client or sends to it; those count toward it too. Once the count
|
|
129
|
+
has come, a checkpoint whose state waits on exactly the calls the caller was
|
|
130
|
+
given also ends the step (#550). The step's calls are the rest.
|
|
128
131
|
|
|
129
132
|
These are retention limits. A missing, expired, restarted, busy, incompatible,
|
|
130
133
|
or full owner uses the original history reconstruction and stateless decoder
|
|
@@ -312,7 +315,10 @@ portable data folder. The newest 20 are kept. A file holds the Run's
|
|
|
312
315
|
frames in order, both ways, as each frame's time (ms from the Run's
|
|
313
316
|
start), size, kind (text, thinking, checkpoint, heartbeat, ...), field
|
|
314
317
|
numbers and sizes, and the field numbers and sizes one level inside them,
|
|
315
|
-
plus the error and the model id.
|
|
318
|
+
plus the error and the model id. A few of Cursor's numbers are kept too: a
|
|
319
|
+
tool count's value, which kind of call (the field number of Cursor's tool
|
|
320
|
+
call) a call update is about, how many calls a checkpoint waits on, and,
|
|
321
|
+
for a Run that went without the native session, why. No prompt, reply or thinking text, tool
|
|
316
322
|
name or argument, blob or checkpoint content, token, header, account or
|
|
317
323
|
conversation id is written; the error's message has anything shaped like
|
|
318
324
|
an email, id or token taken out. Look the file over before you attach it.
|
package/index.mjs
CHANGED
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
//
|
|
29
29
|
// OpenCode speaks chat completions to it (@ai-sdk/openai-compatible); the
|
|
30
30
|
// fetch here answers them.
|
|
31
|
-
import { NATIVE_LIMITS, nativeSessions, NativeSessionError, nativeFields, nativeMcpArgs, Quiet, setWireTrace } from "./native-session.mjs"
|
|
31
|
+
import { NATIVE_LIMITS, nativeSessions, NativeSessionError, nativeFields, nativeMcpArgs, pendingCalls, Quiet, setWireTrace, StepCalls } from "./native-session.mjs"
|
|
32
32
|
import { isDeepStrictEqual } from "node:util"
|
|
33
33
|
import http2 from "node:http2"
|
|
34
34
|
import { STATUS_CODES } from "node:http"
|
|
@@ -2747,7 +2747,7 @@ async function runNative({ tok, base, id, tools, conv, convID, signal, maxMode,
|
|
|
2747
2747
|
|
|
2748
2748
|
// runOnce is one Run of the chat on the agent API at base: an async
|
|
2749
2749
|
// iterator of the answer's pieces, or the error it failed with before any.
|
|
2750
|
-
async function runOnce({ tok, base, id, tools, conv, convID, signal, maxMode, params }) {
|
|
2750
|
+
async function runOnce({ tok, base, id, tools, conv, convID, signal, maxMode, params, why }) {
|
|
2751
2751
|
// the Run's conversation_id, a new one when nothing names the session:
|
|
2752
2752
|
// its usage event is found by it
|
|
2753
2753
|
const cid = convID || randomUUID()
|
|
@@ -2778,7 +2778,7 @@ async function runOnce({ tok, base, id, tools, conv, convID, signal, maxMode, pa
|
|
|
2778
2778
|
let closed = false
|
|
2779
2779
|
let heartbeat
|
|
2780
2780
|
// a Run Cursor stops answering ends, as a retained one does (#2672)
|
|
2781
|
-
const quiet = new Quiet(NATIVE_LIMITS, () => close(), { path: "stateless", model: id })
|
|
2781
|
+
const quiet = new Quiet(NATIVE_LIMITS, () => close(), { path: "stateless", model: id, why })
|
|
2782
2782
|
const send = (msg) => {
|
|
2783
2783
|
if (!closed && !req.destroyed && req.writable) {
|
|
2784
2784
|
quiet.trace?.frame("out", msg)
|
|
@@ -2822,6 +2822,7 @@ async function runOnce({ tok, base, id, tools, conv, convID, signal, maxMode, pa
|
|
|
2822
2822
|
close,
|
|
2823
2823
|
stepUsage: book.own,
|
|
2824
2824
|
owedUsage: book.owed,
|
|
2825
|
+
note: (name) => quiet.trace?.event(name),
|
|
2825
2826
|
})
|
|
2826
2827
|
const wrapped = (async function* () {
|
|
2827
2828
|
try {
|
|
@@ -2852,7 +2853,7 @@ async function runOnce({ tok, base, id, tools, conv, convID, signal, maxMode, pa
|
|
|
2852
2853
|
|
|
2853
2854
|
// decode follows the server's messages into the answer's pieces until the
|
|
2854
2855
|
// turn ends, or the model has made its tool calls.
|
|
2855
|
-
async function* decode(stream, { send, blobs, tools, estimate, close, stepUsage, owedUsage }) {
|
|
2856
|
+
async function* decode(stream, { send, blobs, tools, estimate, close, stepUsage, owedUsage, note, countWait = NATIVE_LIMITS.countWait }) {
|
|
2856
2857
|
let calls = 0
|
|
2857
2858
|
// answered: the step's calls answered here (a refusal, the MCP tool
|
|
2858
2859
|
// list), which Cursor's count of the step's calls takes in too (#142)
|
|
@@ -2860,14 +2861,18 @@ async function* decode(stream, { send, blobs, tools, estimate, close, stepUsage,
|
|
|
2860
2861
|
// ended: Cursor said what the turn used
|
|
2861
2862
|
let ended = false
|
|
2862
2863
|
let listed = 0
|
|
2864
|
+
// what Cursor said of the step's calls: those it completed by itself
|
|
2865
|
+
// count toward listed too, and never come as an exec (plugins#550)
|
|
2866
|
+
const step = new StepCalls(fields)
|
|
2863
2867
|
// stepDone is whether every call the step listed has come. A step whose
|
|
2864
2868
|
// calls were all answered here goes on on Cursor's side, under a count of
|
|
2865
2869
|
// its own.
|
|
2866
2870
|
const stepDone = () => {
|
|
2867
|
-
if (!listed || calls + answered < listed) return false
|
|
2871
|
+
if (!listed || calls + answered + step.completed < listed) return false
|
|
2868
2872
|
if (calls > 0) return true
|
|
2869
2873
|
listed = 0
|
|
2870
2874
|
answered = 0
|
|
2875
|
+
step.nextStep()
|
|
2871
2876
|
return false
|
|
2872
2877
|
}
|
|
2873
2878
|
let said = 0
|
|
@@ -2932,8 +2937,48 @@ async function* decode(stream, { send, blobs, tools, estimate, close, stepUsage,
|
|
|
2932
2937
|
return finish(owedUsage ? await owedUsage(calls === 0).catch(() => null) : null)
|
|
2933
2938
|
}
|
|
2934
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
|
+
}
|
|
2935
2971
|
try {
|
|
2936
|
-
for
|
|
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()
|
|
2937
2982
|
if (f.end) {
|
|
2938
2983
|
const e = failure(200, f.data)
|
|
2939
2984
|
if (e.status < 200 || e.status > 299) yield { error: e }
|
|
@@ -2984,6 +3029,16 @@ async function* decode(stream, { send, blobs, tools, estimate, close, stepUsage,
|
|
|
2984
3029
|
return
|
|
2985
3030
|
}
|
|
2986
3031
|
break
|
|
3032
|
+
case 2: // a call started on the client
|
|
3033
|
+
case 3: // a call completed: by Cursor itself when it never started here
|
|
3034
|
+
case 7: // a call the model is writing
|
|
3035
|
+
// still named in an empty reply's list: only their ids are read
|
|
3036
|
+
count(seen.updates, u.num)
|
|
3037
|
+
if (step.update(u.num, uf) && (note?.("completed by Cursor"), stepDone())) {
|
|
3038
|
+
yield await settled()
|
|
3039
|
+
return
|
|
3040
|
+
}
|
|
3041
|
+
break
|
|
2987
3042
|
default:
|
|
2988
3043
|
count(seen.updates, u.num)
|
|
2989
3044
|
}
|
|
@@ -2991,6 +3046,7 @@ async function* decode(stream, { send, blobs, tools, estimate, close, stepUsage,
|
|
|
2991
3046
|
break
|
|
2992
3047
|
case 2: {
|
|
2993
3048
|
// exec_server_message: the server asks the client to run something
|
|
3049
|
+
step.exec(m.data)
|
|
2994
3050
|
const call = exec(m.data, tools, send, closeExec, seen.refused)
|
|
2995
3051
|
if (call) {
|
|
2996
3052
|
said += call.args.length
|
|
@@ -3003,6 +3059,18 @@ async function* decode(stream, { send, blobs, tools, estimate, close, stepUsage,
|
|
|
3003
3059
|
}
|
|
3004
3060
|
break
|
|
3005
3061
|
}
|
|
3062
|
+
case 3:
|
|
3063
|
+
// conversation_checkpoint_update: Cursor's state. Once the step
|
|
3064
|
+
// has its count, a state waiting on just the calls handed to the
|
|
3065
|
+
// caller is a step Cursor has finished: what more the count
|
|
3066
|
+
// says will not come (plugins#550)
|
|
3067
|
+
if (listed && calls > 0 && pendingCalls(m.data, fields) === calls) {
|
|
3068
|
+
note?.("waited on")
|
|
3069
|
+
yield await settled()
|
|
3070
|
+
return
|
|
3071
|
+
}
|
|
3072
|
+
count(seen.messages, m.num)
|
|
3073
|
+
break
|
|
3006
3074
|
case 4: {
|
|
3007
3075
|
// kv_server_message: a blob wanted, or one to keep
|
|
3008
3076
|
const kv = fields(m.data)
|
|
@@ -3033,7 +3101,10 @@ async function* decode(stream, { send, blobs, tools, estimate, close, stepUsage,
|
|
|
3033
3101
|
} catch (e) {
|
|
3034
3102
|
// gone quiet is no answer, calls or not: the step's count never came
|
|
3035
3103
|
if (e?.quiet) {
|
|
3036
|
-
|
|
3104
|
+
// with what came of the step's calls, as the native path's batch
|
|
3105
|
+
// error says it (plugins#550)
|
|
3106
|
+
const batch = calls || listed ? `; the step's tool count ${listed || "not sent"}: ${calls} calls came, ${answered} answered here, ${step.completed} completed by Cursor` : ""
|
|
3107
|
+
yield { error: { status: e.status, message: batch ? e.message.replace(/( \(try again\))?$/, batch + "$1") : e.message } }
|
|
3037
3108
|
return
|
|
3038
3109
|
}
|
|
3039
3110
|
if (calls > 0 && /EOF/.test(e?.message ?? "")) {
|
|
@@ -3041,6 +3112,9 @@ async function* decode(stream, { send, blobs, tools, estimate, close, stepUsage,
|
|
|
3041
3112
|
return
|
|
3042
3113
|
}
|
|
3043
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(() => {})
|
|
3044
3118
|
}
|
|
3045
3119
|
}
|
|
3046
3120
|
|
|
@@ -3207,10 +3281,11 @@ async function answerOf(auth, chat, signal, session = "") {
|
|
|
3207
3281
|
// uses the original history builder/decoder without cancelling a live parent.
|
|
3208
3282
|
const native = prepared.fallback ? null : nativeSessions.create(prepared, { id, maxMode, params }, { pb, exec, usageOf, failure })
|
|
3209
3283
|
const args = { tok, base, id, tools, conv, convID, signal, maxMode, params }
|
|
3210
|
-
let response = native ? await runNative({ ...args, native }) : await runOnce(args)
|
|
3284
|
+
let response = native ? await runNative({ ...args, native }) : await runOnce({ ...args, why: prepared.fallback || "retention capacity or reservation unavailable" })
|
|
3211
3285
|
const rebuild = async () => {
|
|
3212
|
-
const
|
|
3213
|
-
fallback
|
|
3286
|
+
const why = response.error?.continuityReason || "missing-batch-count"
|
|
3287
|
+
const fallback = await runOnce({ ...args, why })
|
|
3288
|
+
fallback.diagnostic = "stateless; bootstrap=imported-history; reason=" + why
|
|
3214
3289
|
return fallback
|
|
3215
3290
|
}
|
|
3216
3291
|
// A missing or conflicting native batch count may be retried once,
|
package/native-session.mjs
CHANGED
|
@@ -9,6 +9,12 @@ export const NATIVE_LIMITS = {
|
|
|
9
9
|
resultWait: 120_000,
|
|
10
10
|
lifetime: 15 * 60_000,
|
|
11
11
|
batchWait: 30_000,
|
|
12
|
+
// How long a stateless step whose calls came waits for Cursor's count of
|
|
13
|
+
// them (update 27) with nothing but heartbeats before it hands the calls
|
|
14
|
+
// that came to the caller (plugins#550). A count comes within ms of the
|
|
15
|
+
// step's last call; a Run that sent calls and then only heartbeats has
|
|
16
|
+
// stopped, and the caller's next request goes on from the calls' results.
|
|
17
|
+
countWait: 60_000,
|
|
12
18
|
drain: 6000,
|
|
13
19
|
replayWait: 120_000,
|
|
14
20
|
blobBytes: 64 << 20,
|
|
@@ -55,7 +61,13 @@ const failureOf = (e) => ({ status: e?.status ?? 502, message: e?.message ?? Str
|
|
|
55
61
|
|
|
56
62
|
// kindOf is what a frame of AgentServerMessage is, for Quiet: a heartbeat
|
|
57
63
|
// alone, or what an error names as the last thing Cursor sent
|
|
58
|
-
|
|
64
|
+
// (names after cursor-agent's agent.v1 InteractionUpdate, 2026.10.01)
|
|
65
|
+
const UPDATES = {
|
|
66
|
+
1: "text", 2: "tool call started", 3: "tool call completed", 4: "thinking", 5: "thinking done",
|
|
67
|
+
6: "user message", 7: "partial tool call", 8: "token count", 9: "summary", 10: "summary started",
|
|
68
|
+
11: "summary done", 12: "shell output", 14: "turn end", 15: "tool call delta", 16: "step started",
|
|
69
|
+
17: "step done", 27: "tool count",
|
|
70
|
+
}
|
|
59
71
|
const MESSAGES = { 2: "tool call", 3: "checkpoint", 4: "blob request" }
|
|
60
72
|
function kindOf(f) {
|
|
61
73
|
if (f.end) return "stream end"
|
|
@@ -249,6 +261,26 @@ export function scrub(s) {
|
|
|
249
261
|
.replace(/[A-Za-z0-9_\-+/=.]{24,}/g, "<redacted>")
|
|
250
262
|
.slice(0, 2000)
|
|
251
263
|
}
|
|
264
|
+
// detail is what a trace keeps of an inbound frame past its shape, all of
|
|
265
|
+
// it numbers of Cursor's protocol, never content (plugins#550): a tool
|
|
266
|
+
// count's number, the kind of call (ToolCall's field) a call update is
|
|
267
|
+
// of, and how many calls a checkpoint's state waits on
|
|
268
|
+
function detail(data) {
|
|
269
|
+
const out = {}
|
|
270
|
+
for (const m of nativeFields(data)) {
|
|
271
|
+
if (m.num === 1) {
|
|
272
|
+
for (const u of nativeFields(m.data)) {
|
|
273
|
+
if (u.num === 27) out.count = num(nativeFields(u.data), 1)
|
|
274
|
+
else if (u.num === 2 || u.num === 3 || u.num === 7) {
|
|
275
|
+
const tc = field(nativeFields(u.data), 2)
|
|
276
|
+
const kind = tc && nativeFields(tc.data)[0]
|
|
277
|
+
if (kind) out.tool = kind.wire === 2 ? [kind.num, nativeFields(kind.data)[0]?.num ?? 0] : [kind.num]
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
} else if (m.num === 3) out.pending = pendingCalls(m.data)
|
|
281
|
+
}
|
|
282
|
+
return out
|
|
283
|
+
}
|
|
252
284
|
export class WireTrace {
|
|
253
285
|
static open(meta) {
|
|
254
286
|
let dir = ""
|
|
@@ -258,6 +290,8 @@ export class WireTrace {
|
|
|
258
290
|
constructor(dir, meta = {}) {
|
|
259
291
|
this.dir = dir
|
|
260
292
|
this.meta = { path: String(meta.path ?? ""), model: String(meta.model ?? "") }
|
|
293
|
+
// why a Run went stateless: the retention fallback's reason
|
|
294
|
+
if (meta.why) this.meta.why = String(meta.why)
|
|
261
295
|
this.started = Date.now()
|
|
262
296
|
this.head = []
|
|
263
297
|
this.tail = []
|
|
@@ -281,6 +315,7 @@ export class WireTrace {
|
|
|
281
315
|
if (dir === "in") entry.kind = kind ?? "heartbeat"
|
|
282
316
|
if (dir === "in" && f.end) entry.end = true
|
|
283
317
|
else try { entry.fields = shape(data, true) } catch { entry.malformed = true }
|
|
318
|
+
if (dir === "in" && !f.end) try { Object.assign(entry, detail(data)) } catch {}
|
|
284
319
|
this.push(entry)
|
|
285
320
|
}
|
|
286
321
|
event(name, extra = {}) {
|
|
@@ -438,6 +473,75 @@ const field = (fs, n) => fs.find((f) => f.num === n)
|
|
|
438
473
|
const str = (fs, n) => field(fs, n)?.data.toString("utf8") ?? ""
|
|
439
474
|
const num = (fs, n) => field(fs, n)?.n ?? 0
|
|
440
475
|
|
|
476
|
+
// StepCalls is what Cursor says of a step's calls besides its execs
|
|
477
|
+
// (plugins#550). InteractionUpdate 2 (tool_call_started), 3
|
|
478
|
+
// (tool_call_completed) and 7 (partial_tool_call) each carry the call's
|
|
479
|
+
// call_id (field 1). A call Cursor completed by itself, never started on
|
|
480
|
+
// the client and never sent as an exec, is in tool_requests_listed's count
|
|
481
|
+
// all the same, and no exec of it will come: 0.2.34 waited for it while
|
|
482
|
+
// Cursor waited for the one call it had sent, 240 s, heartbeats only.
|
|
483
|
+
// parse is the path's field reader.
|
|
484
|
+
export class StepCalls {
|
|
485
|
+
constructor(parse) {
|
|
486
|
+
this.parse = parse
|
|
487
|
+
this.started = new Set()
|
|
488
|
+
this.execs = new Set() // the short strings of each exec's args: its tool_call_id among them
|
|
489
|
+
this.partial = new Set() // calls the model is writing in this step
|
|
490
|
+
this.byCursor = new Set()
|
|
491
|
+
}
|
|
492
|
+
// update takes an InteractionUpdate's own field (num) and its fields; it
|
|
493
|
+
// says whether a call was completed by Cursor: one this step wrote (7)
|
|
494
|
+
// and completed (3) with no start on the client (2) and no exec. A
|
|
495
|
+
// completion of an earlier step's call is never counted again.
|
|
496
|
+
update(n, uf) {
|
|
497
|
+
if (n !== 2 && n !== 3 && n !== 7) return false
|
|
498
|
+
const id = str(uf, 1)
|
|
499
|
+
if (!id) return false
|
|
500
|
+
if (n === 2) this.started.add(id)
|
|
501
|
+
if (n === 7) this.partial.add(id)
|
|
502
|
+
if (n !== 3) return false
|
|
503
|
+
if (!this.partial.has(id) || this.started.has(id) || this.execs.has(id) || this.byCursor.has(id)) return false
|
|
504
|
+
this.byCursor.add(id)
|
|
505
|
+
return true
|
|
506
|
+
}
|
|
507
|
+
// exec notes an exec's args, whatever its kind: each kind keeps its
|
|
508
|
+
// tool_call_id at a field of its own (ShellArgs 4, ReadArgs 2, McpArgs 3)
|
|
509
|
+
exec(data) {
|
|
510
|
+
try {
|
|
511
|
+
for (const e of this.parse(data)) {
|
|
512
|
+
if (e.wire !== 2 || e.num === 15 || e.num === 19) continue
|
|
513
|
+
for (const a of this.parse(e.data)) if (a.wire === 2 && a.data.length && a.data.length <= 512) this.execs.add(a.data.toString("utf8"))
|
|
514
|
+
}
|
|
515
|
+
} catch {}
|
|
516
|
+
}
|
|
517
|
+
get completed() {
|
|
518
|
+
return this.byCursor.size
|
|
519
|
+
}
|
|
520
|
+
// writing is how many calls the step wrote (7) that are neither started
|
|
521
|
+
// on the client, sent as an exec, nor completed by Cursor: a call of
|
|
522
|
+
// Cursor's own still running, or one still to be sent
|
|
523
|
+
get writing() {
|
|
524
|
+
let n = 0
|
|
525
|
+
for (const id of this.partial) if (!this.started.has(id) && !this.execs.has(id) && !this.byCursor.has(id)) n++
|
|
526
|
+
return n
|
|
527
|
+
}
|
|
528
|
+
// nextStep starts the next step's count
|
|
529
|
+
nextStep() {
|
|
530
|
+
this.partial.clear()
|
|
531
|
+
this.byCursor.clear()
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
// pendingCalls is how many calls a checkpoint (ConversationStateStructure)
|
|
535
|
+
// says Cursor waits on: its pending_tool_calls, field 4; null when it
|
|
536
|
+
// can't be read
|
|
537
|
+
export function pendingCalls(data, parse = nativeFields) {
|
|
538
|
+
try {
|
|
539
|
+
return parse(data).filter((f) => f.num === 4).length
|
|
540
|
+
} catch {
|
|
541
|
+
return null
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
441
545
|
function argsOf(s) {
|
|
442
546
|
try {
|
|
443
547
|
const v = typeof s === "string" ? JSON.parse(s) : s
|
|
@@ -737,6 +841,8 @@ class NativeSession {
|
|
|
737
841
|
this.ac = new AbortController()
|
|
738
842
|
this.execs = new Map()
|
|
739
843
|
this.nativeCalls = new Map()
|
|
844
|
+
// what Cursor said of the step's calls besides its execs (plugins#550)
|
|
845
|
+
this.stepCalls = new StepCalls(nativeFields)
|
|
740
846
|
this.blobs = new Map()
|
|
741
847
|
this.knownHashes = new Set()
|
|
742
848
|
this.blobSize = 0
|
|
@@ -934,6 +1040,7 @@ class NativeSession {
|
|
|
934
1040
|
// caller's results, or after a step whose calls were all answered here.
|
|
935
1041
|
nextStep(estimate) {
|
|
936
1042
|
this.steps++
|
|
1043
|
+
this.stepCalls.nextStep()
|
|
937
1044
|
this.quiet.nextStep(this.steps)
|
|
938
1045
|
this.stepSaid = this.said
|
|
939
1046
|
if (estimate != null) this.stepEstimate = estimate
|
|
@@ -951,7 +1058,7 @@ class NativeSession {
|
|
|
951
1058
|
this.batchTimer = later(() => {
|
|
952
1059
|
const s = this.segment
|
|
953
1060
|
if (s.expected == null && s.calls.length && !this.resultSubmitted) this.missingCount()
|
|
954
|
-
else this.fail({ status: 502, message: `Cursor did not complete its declared native tool batch: ${s.calls.length} of ${s.expected ?? "?"} calls came, ${s.answered} answered here, none since ${Date.now() - since} ms` })
|
|
1061
|
+
else this.fail({ status: 502, message: `Cursor did not complete its declared native tool batch: ${s.calls.length} of ${s.expected ?? "?"} calls came, ${s.answered} answered here${this.stepCalls.completed ? `, ${this.stepCalls.completed} completed by Cursor` : ""}, none since ${Date.now() - since} ms` })
|
|
955
1062
|
}, this.limits.batchWait)
|
|
956
1063
|
}
|
|
957
1064
|
missingCount() {
|
|
@@ -967,6 +1074,23 @@ class NativeSession {
|
|
|
967
1074
|
if (this.batchTimer) this.waitForBatch()
|
|
968
1075
|
this.sealBatch()
|
|
969
1076
|
}
|
|
1077
|
+
// A call of the step Cursor completed by itself (plugins#550): in its
|
|
1078
|
+
// count, and never sent as an exec.
|
|
1079
|
+
completedByCursor() {
|
|
1080
|
+
if (this.terminal || this.state === "awaiting-results") return
|
|
1081
|
+
if (this.batchTimer) this.waitForBatch()
|
|
1082
|
+
this.sealBatch()
|
|
1083
|
+
}
|
|
1084
|
+
// A checkpoint whose state waits on just the calls handed to the caller,
|
|
1085
|
+
// after the step's count, is a step Cursor has finished: what more the
|
|
1086
|
+
// count says will not come (plugins#550).
|
|
1087
|
+
waitedOn(data) {
|
|
1088
|
+
const s = this.segment
|
|
1089
|
+
if (this.terminal || this.state === "awaiting-results" || s.closed || s.expected == null || !s.calls.length) return
|
|
1090
|
+
if (pendingCalls(data) !== s.calls.length) return
|
|
1091
|
+
s.waitedOn = true
|
|
1092
|
+
this.sealBatch()
|
|
1093
|
+
}
|
|
970
1094
|
listed(count) {
|
|
971
1095
|
if (!Number.isSafeInteger(count) || count < 1) invalid("invalid native tool count", 502)
|
|
972
1096
|
if (this.segment.expected != null && this.segment.expected !== count) {
|
|
@@ -986,7 +1110,8 @@ class NativeSession {
|
|
|
986
1110
|
if (segment.expected == null) return
|
|
987
1111
|
const made = segment.calls.length + segment.answered
|
|
988
1112
|
if (made > segment.expected) invalid("native tool batch exceeded its declared count", 502)
|
|
989
|
-
|
|
1113
|
+
// the calls Cursor completed by itself make up the rest (plugins#550)
|
|
1114
|
+
if ((made + this.stepCalls.completed < segment.expected && !segment.waitedOn) || segment.closed) return
|
|
990
1115
|
if (!segment.calls.length) {
|
|
991
1116
|
// Every call of the step was answered here: Cursor goes on with the
|
|
992
1117
|
// turn by itself, and its next step lists a count of its own.
|
|
@@ -1183,10 +1308,14 @@ class NativeSession {
|
|
|
1183
1308
|
if (this.error) return
|
|
1184
1309
|
} else if (u.num === 13) {
|
|
1185
1310
|
if (this.batchTimer && !this.terminal && this.state !== "awaiting-results") this.waitForBatch()
|
|
1311
|
+
} else if ((u.num === 2 || u.num === 3 || u.num === 7) && this.stepCalls.update(u.num, uf)) {
|
|
1312
|
+
this.completedByCursor()
|
|
1313
|
+
if (this.error) return
|
|
1186
1314
|
}
|
|
1187
1315
|
}
|
|
1188
1316
|
break
|
|
1189
1317
|
case 2: {
|
|
1318
|
+
this.stepCalls.exec(m.data)
|
|
1190
1319
|
const c = exec(m.data, this.chatTools, (b) => this.send(b), (id) => this.execClose(id), null, true)
|
|
1191
1320
|
if (c) this.acceptCall(c)
|
|
1192
1321
|
// request_context is Cursor asking for the environment, not a call
|
|
@@ -1195,6 +1324,7 @@ class NativeSession {
|
|
|
1195
1324
|
}
|
|
1196
1325
|
case 3:
|
|
1197
1326
|
this.checkpointReceived(m.data)
|
|
1327
|
+
this.waitedOn(m.data)
|
|
1198
1328
|
break
|
|
1199
1329
|
case 4: {
|
|
1200
1330
|
const kv = nativeFields(m.data)
|
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.36",
|
|
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",
|