@magpie-community/opencode-cursor-auth 0.2.13 → 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.
@@ -0,0 +1,880 @@
1
+ import { createHash, randomBytes } from "node:crypto"
2
+
3
+ // Retention is process-local and optional. A lost owner rebuilds Chat history
4
+ // on a fresh Run; it never resubmits a result to an uncertain native exec.
5
+ // Checkpoints are retained evidence, not restart handles.
6
+ export const NATIVE_LIMITS = {
7
+ resultWait: 120_000,
8
+ lifetime: 15 * 60_000,
9
+ batchWait: 30_000,
10
+ drain: 6000,
11
+ replayWait: 120_000,
12
+ blobBytes: 64 << 20,
13
+ outputBytes: 16 << 20,
14
+ argumentBytes: 1 << 20,
15
+ argumentFields: 100_000,
16
+ argumentDepth: 64,
17
+ outboundBytes: 16 << 20,
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,
22
+ perAccount: 8,
23
+ perProcess: 32,
24
+ }
25
+
26
+ const HANDLE = /^call_cursor_[a-f0-9]+_\d+_\d+$/
27
+ const hash = (s) => createHash("sha256").update(s).digest("hex")
28
+ const canonical = (v) => JSON.stringify(v, (_k, x) => x && typeof x === "object" && !Array.isArray(x)
29
+ ? Object.fromEntries(Object.keys(x).sort().map((k) => [k, x[k]])) : x)
30
+ const bytes = (v) => Buffer.byteLength(canonical(v))
31
+ const later = (fn, ms) => {
32
+ const timer = setTimeout(fn, ms)
33
+ timer.unref?.()
34
+ return timer
35
+ }
36
+
37
+ export class NativeSessionError extends Error {
38
+ constructor(status, message) {
39
+ super(message)
40
+ this.status = status
41
+ }
42
+ }
43
+ const invalid = (message, status = 409) => { throw new NativeSessionError(status, message) }
44
+ const failureOf = (e) => ({ status: e?.status ?? 502, message: e?.message ?? String(e) })
45
+
46
+ // Unlike the compatibility codec, malformed protobuf tails must fail a live
47
+ // continuation. Dropping one could turn a truncated batch into a valid handoff.
48
+ export function nativeFields(input, maxFields = Infinity) {
49
+ const b = Buffer.from(input)
50
+ const out = []
51
+ let at = 0
52
+ const uv = () => {
53
+ let n = 0
54
+ let scale = 1
55
+ for (let i = 0; i < 10 && at < b.length; i++) {
56
+ const c = b[at++]
57
+ n += (c & 127) * scale
58
+ if (c < 128 && Number.isSafeInteger(n)) return n
59
+ scale *= 128
60
+ }
61
+ invalid("malformed native protobuf", 502)
62
+ }
63
+ while (at < b.length) {
64
+ const key = uv()
65
+ const num = Math.floor(key / 8)
66
+ const wire = key % 8
67
+ if (!num || num > 0x1fffffff) invalid("malformed native protobuf field", 502)
68
+ let n = 0
69
+ let data = Buffer.alloc(0)
70
+ if (wire === 0) n = uv()
71
+ else if (wire === 2 || wire === 1 || wire === 5) {
72
+ const size = wire === 2 ? uv() : wire === 1 ? 8 : 4
73
+ if (at + size > b.length) invalid("truncated native protobuf", 502)
74
+ data = b.subarray(at, at + size)
75
+ at += size
76
+ } else invalid("unsupported native protobuf wire type", 502)
77
+ if (out.length >= maxFields) invalid("native protobuf field limit reached", 502)
78
+ out.push({ num, wire, n, data })
79
+ }
80
+ return out
81
+ }
82
+ // Decode every known Value kind, including overwritten oneof/map fields, so
83
+ // corrupt bytes cannot become different, valid JSON through prefix decoding.
84
+ // Missing scalar/map values keep protobuf defaults; unknown fields are skipped
85
+ // only after their complete wire envelope has passed validation.
86
+ export function nativeMcpArgs(input, limits = NATIVE_LIMITS) {
87
+ if (input.length > limits.argumentBytes) invalid("native MCP argument byte limit reached", 502)
88
+ let remaining = limits.argumentFields
89
+ const read = (b) => {
90
+ const fs = nativeFields(b, remaining)
91
+ remaining -= fs.length
92
+ return fs
93
+ }
94
+ const typed = (f, wire) => {
95
+ if (f.wire !== wire) invalid("invalid native MCP argument field type", 502)
96
+ }
97
+ const utf8 = (b) => {
98
+ try { return new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(b) }
99
+ catch { invalid("invalid UTF-8 in native MCP arguments", 502) }
100
+ }
101
+ function mapEntry(b, depth) {
102
+ let key = "", value = null
103
+ for (const f of read(b)) {
104
+ if (f.num === 1) { typed(f, 2); key = utf8(f.data) }
105
+ else if (f.num === 2) { typed(f, 2); value = decodeValue(f.data, depth) }
106
+ }
107
+ return [key, value]
108
+ }
109
+ function decodeStruct(b, depth) {
110
+ const entries = []
111
+ for (const f of read(b)) if (f.num === 1) {
112
+ typed(f, 2)
113
+ entries.push(mapEntry(f.data, depth))
114
+ }
115
+ return Object.fromEntries(entries)
116
+ }
117
+ function decodeValue(b, depth = 0) {
118
+ if (depth > limits.argumentDepth) invalid("native MCP argument nesting limit reached", 502)
119
+ let value = null
120
+ for (const f of read(b)) {
121
+ if (f.num < 1 || f.num > 6) continue
122
+ typed(f, [0, 1, 2, 0, 2, 2][f.num - 1])
123
+ switch (f.num) {
124
+ case 1:
125
+ if (f.n !== 0) invalid("invalid native protobuf NullValue", 502)
126
+ value = null
127
+ break
128
+ case 2:
129
+ value = f.data.readDoubleLE(0)
130
+ if (!Number.isFinite(value)) invalid("nonfinite native MCP argument number", 502)
131
+ break
132
+ case 3: value = utf8(f.data); break
133
+ case 4: value = f.n !== 0; break
134
+ case 5: value = decodeStruct(f.data, depth + 1); break
135
+ case 6:
136
+ value = []
137
+ for (const item of read(f.data)) if (item.num === 1) {
138
+ typed(item, 2)
139
+ value.push(decodeValue(item.data, depth + 1))
140
+ }
141
+ break
142
+ }
143
+ }
144
+ return value
145
+ }
146
+ const fs = read(input), entries = []
147
+ for (const f of fs) {
148
+ if ([1, 3, 4, 5, 9].includes(f.num)) { typed(f, 2); utf8(f.data) }
149
+ else if (f.num === 2) { typed(f, 2); entries.push(mapEntry(f.data, 0)) }
150
+ else if (f.num === 6) typed(f, 2)
151
+ else if (f.num === 7) typed(f, 0)
152
+ }
153
+ return { fields: fs, args: Object.fromEntries(entries) }
154
+ }
155
+
156
+ const field = (fs, n) => fs.find((f) => f.num === n)
157
+ const str = (fs, n) => field(fs, n)?.data.toString("utf8") ?? ""
158
+ const num = (fs, n) => field(fs, n)?.n ?? 0
159
+
160
+ function argsOf(s) {
161
+ try {
162
+ const v = typeof s === "string" ? JSON.parse(s) : s
163
+ if (v && typeof v === "object" && !Array.isArray(v)) return v
164
+ } catch {}
165
+ invalid("invalid tool arguments in native continuation", 400)
166
+ }
167
+ // These conversions only decide whether a retained exec can be continued.
168
+ // Unsupported parts make that request use the legacy history builder instead
169
+ // of imposing a new Chat request contract on all callers.
170
+ function assistantDisplay(message) {
171
+ const parts = typeof message.content === "string" ? [{ type: "text", text: message.content }] : message.content ?? []
172
+ if (!Array.isArray(parts) || parts.some((p) => !["text", "reasoning"].includes(p?.type) || typeof p.text !== "string")) invalid("unsupported native assistant display")
173
+ const reasoning = [message.reasoning_content, message.reasoning, message.reasoning_text].find((v) => typeof v === "string" && v.trim()) ?? ""
174
+ return {
175
+ text: parts.filter((p) => p.type === "text").map((p) => p.text).join(""),
176
+ reasoning: reasoning + parts.filter((p) => p.type === "reasoning").map((p) => p.text).join(""),
177
+ }
178
+ }
179
+ function resultParts(message, limits) {
180
+ const parts = typeof message.content === "string" ? [{ type: "text", text: message.content }] : message.content
181
+ if (!Array.isArray(parts)) invalid("unsupported native tool result")
182
+ let retained = 0
183
+ return parts.map((part) => {
184
+ if (part?.type === "text" && typeof part.text === "string") {
185
+ retained += Buffer.byteLength(part.text)
186
+ if (retained > limits.outputBytes) invalid("native tool result limit reached")
187
+ return { text: part.text }
188
+ }
189
+ const url = part?.type === "image_url" ? (typeof part.image_url === "string" ? part.image_url : part.image_url?.url)
190
+ : part?.type === "image" && part.source?.type === "base64" ? `data:${part.source.media_type};base64,${part.source.data}` : ""
191
+ const image = /^data:(image\/(?:png|jpeg|gif|webp));base64,([A-Za-z0-9+/]*={0,2})$/.exec(url ?? "")
192
+ if (!image) invalid("unsupported native tool result content")
193
+ const data = Buffer.from(image[2], "base64")
194
+ retained += data.length
195
+ if (retained > limits.outputBytes) invalid("native tool result limit reached")
196
+ if (!data.length || data.toString("base64").replace(/=+$/, "") !== image[2].replace(/=+$/, "")) invalid("invalid native result image")
197
+ return { data, mimeType: image[1] }
198
+ })
199
+ }
200
+ function normalizeMessage(m) {
201
+ if (m?.role !== "assistant") return m
202
+ const { text, reasoning } = assistantDisplay(m)
203
+ const out = { ...m, content: text }
204
+ delete out.reasoning
205
+ delete out.reasoning_content
206
+ delete out.reasoning_text
207
+ if (reasoning) out.reasoning_content = reasoning
208
+ if (m.tool_calls) out.tool_calls = m.tool_calls.map((c) => ({
209
+ ...c, type: c.type ?? "function", function: { ...c.function, arguments: argsOf(c.function?.arguments ?? "{}") },
210
+ }))
211
+ return out
212
+ }
213
+ const historyDigest = (messages) => hash(canonical(messages.map(normalizeMessage)))
214
+ function controlsOf(chat, selector, tools) {
215
+ const controls = { ...chat }
216
+ for (const k of ["messages", "stream", "stream_options"]) delete controls[k]
217
+ return hash(canonical({ selector, controls, tools }))
218
+ }
219
+ const toolAttachment = (m) => m?.role === "user" && Array.isArray(m.content) && m.content.length > 1
220
+ && m.content[0]?.type === "text" && m.content[0].text === "Attached image(s) from tool result:"
221
+ && m.content.slice(1).every((p) => p?.type === "image_url")
222
+ // A complete result batch is validated before reserving or writing any exec.
223
+ // Only the current exported assistant may omit display reasoning. All earlier
224
+ // accepted history must match, including instruction order and attachments.
225
+ function matchResults(batch, messages) {
226
+ if (historyDigest(messages.slice(0, batch.prefixLength)) !== batch.prefixDigest) invalid("native continuation history diverged")
227
+ const suffix = messages.slice(batch.prefixLength)
228
+ const assistants = []
229
+ while (suffix[assistants.length]?.role === "assistant") assistants.push(suffix[assistants.length])
230
+ if (!assistants.length) invalid("native continuation is missing its assistant tool calls", 400)
231
+ let content = ""
232
+ let reasoning = ""
233
+ const calls = []
234
+ for (const m of assistants) {
235
+ const unknown = Object.keys(m).filter((k) => !["role", "content", "tool_calls", "reasoning", "reasoning_content", "reasoning_text"].includes(k))
236
+ if (unknown.length) invalid("unsupported assistant fields in native continuation", 400)
237
+ const display = assistantDisplay(m)
238
+ content += display.text
239
+ reasoning += display.reasoning
240
+ for (const c of m.tool_calls ?? []) {
241
+ if (c.type && c.type !== "function") invalid("unsupported native continuation tool call", 400)
242
+ calls.push({ id: c.id, name: c.function?.name, args: argsOf(c.function?.arguments ?? "{}") })
243
+ }
244
+ }
245
+ if (content !== batch.content || reasoning && reasoning !== batch.reasoning) invalid("native continuation assistant output changed")
246
+ const expected = batch.calls.map((c) => ({ id: c.id, name: c.name, args: argsOf(c.args) }))
247
+ const sorted = (v) => [...v].sort((a, b) => String(a.id).localeCompare(String(b.id)))
248
+ if (canonical(sorted(calls)) !== canonical(sorted(expected))) invalid("native continuation tool calls changed")
249
+ let results = suffix.slice(assistants.length)
250
+ // Pi's exact image suffix has no call IDs. Only a single complete result
251
+ // gives it unambiguous ownership; all other shapes rebuild legacy history.
252
+ if (batch.calls.length === 1 && results.length === 2 && results[0]?.role === "tool" && toolAttachment(results[1])) {
253
+ const [result, attachment] = results
254
+ const content = typeof result.content === "string" ? [{ type: "text", text: result.content }] : result.content
255
+ if (!Array.isArray(content)) invalid("unsupported native tool result")
256
+ results = [{ ...result, content: [...content, ...attachment.content.slice(1)] }]
257
+ }
258
+ if (results.some((m) => m?.role !== "tool")) invalid("new input cannot be interleaved with pending native results")
259
+ const ids = new Set()
260
+ for (const m of results) {
261
+ if (ids.has(m.tool_call_id)) invalid("duplicate native tool result", 400)
262
+ ids.add(m.tool_call_id)
263
+ if (!expected.some((c) => c.id === m.tool_call_id)) invalid("unknown native tool result", 400)
264
+ }
265
+ if (ids.size !== expected.length) invalid("all parallel native tool results must be submitted together", 400)
266
+ return [...results].sort((a, b) => a.tool_call_id.localeCompare(b.tool_call_id))
267
+ }
268
+
269
+ class Segment {
270
+ constructor(owner, chat) {
271
+ this.owner = owner
272
+ this.prefixLength = chat.messages.length
273
+ this.prefixDigest = historyDigest(chat.messages)
274
+ this.events = []
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
279
+ this.content = ""
280
+ this.reasoning = ""
281
+ this.expected = null
282
+ this.closed = false
283
+ this.clean = false
284
+ this.waiters = new Set()
285
+ }
286
+ wake() {
287
+ for (const wake of this.waiters) wake()
288
+ this.waiters.clear()
289
+ }
290
+ emit(e) {
291
+ this.owner.retain(bytes(e))
292
+ if (e.text) this.content += e.text
293
+ if (e.reasoning) this.reasoning += e.reasoning
294
+ this.events.push(e)
295
+ this.wake()
296
+ }
297
+ finish(stop) {
298
+ for (const [index, c] of this.calls.entries()) {
299
+ const e = { tool: { index, id: c.id, name: c.name, args: c.args } }
300
+ this.owner.retain(bytes(e))
301
+ this.events.push(e)
302
+ }
303
+ this.owner.retain(bytes(stop))
304
+ this.events.push(stop)
305
+ this.closed = true
306
+ this.wake()
307
+ }
308
+ fail(error) {
309
+ // Errors also override a sealed, as yet undelivered handoff.
310
+ this.events = [{ error }]
311
+ this.closed = true
312
+ this.wake()
313
+ }
314
+ iterator({ replay = false } = {}) {
315
+ const segment = this
316
+ let at = 0
317
+ let errorSent = false
318
+ return {
319
+ async next() {
320
+ while (!segment.closed && at >= segment.events.length) await new Promise((r) => segment.waiters.add(r))
321
+ if (!replay && segment.owner.error) {
322
+ if (errorSent) return { done: true }
323
+ errorSent = true
324
+ return { value: { error: segment.owner.error }, done: false }
325
+ }
326
+ if (at < segment.events.length) {
327
+ const event = segment.events[at++]
328
+ if (replay && event.stop) return { value: { ...event, usage: {
329
+ prompt_tokens: 0, completion_tokens: 0, total_tokens: 0,
330
+ prompt_tokens_details: { cached_tokens: 0, cache_write_tokens: 0 },
331
+ magpie_native: { accounting: "replayed" },
332
+ } }, done: false }
333
+ return { value: event, done: false }
334
+ }
335
+ return { done: true }
336
+ },
337
+ async return() {
338
+ if (!replay) await segment.owner.cancel("the Chat response was cancelled")
339
+ return { done: true }
340
+ },
341
+ [Symbol.asyncIterator]() { return this },
342
+ }
343
+ }
344
+ }
345
+
346
+ export class NativeSessionPool {
347
+ constructor(limits = NATIVE_LIMITS) {
348
+ this.limits = limits
349
+ this.entries = new Set()
350
+ this.handles = new Map()
351
+ this.selectors = new Map()
352
+ }
353
+ // selector is conversationID(chat, session): session/cache key AND first
354
+ // user identity. Task subagents sharing a session must not own its parent.
355
+ prepare(tok, chat, selector, tools) {
356
+ const fallback = (reason) => ({ fallback: reason })
357
+ if (!Array.isArray(chat.messages) || bytes(chat) > this.limits.outputBytes) return fallback("history retention limit")
358
+ const account = hash(tok)
359
+ const controls = controlsOf(chat, selector, tools)
360
+ const trailing = []
361
+ const end = chat.messages.length - (toolAttachment(chat.messages.at(-1)) ? 2 : 1)
362
+ for (let i = end; i >= 0 && chat.messages[i]?.role === "tool"; i--) trailing.push(chat.messages[i].tool_call_id)
363
+ const handles = trailing.filter((id) => typeof id === "string" && HANDLE.test(id))
364
+ if (handles.length) {
365
+ // Look up ownership before inspecting native history. Restarted callers
366
+ // only need the same permissive history shape the stateless path accepts.
367
+ const found = handles.map((id) => this.handles.get(id))
368
+ if (found.some((v) => !v)) return fallback("owner unavailable")
369
+ const { owner, batch } = found[0]
370
+ if (found.some((v) => v.owner !== owner || v.batch !== batch)) return fallback("different native batches")
371
+ if (owner.account !== account || owner.controls !== controls || owner.error) return fallback("owner incompatible or expired")
372
+ try {
373
+ const results = matchResults(batch, chat.messages)
374
+ // Encoding is pure for the entire batch before any reservation or write.
375
+ const encoded = results.map((m) => owner.encodeResult(m))
376
+ const digest = hash(canonical(results))
377
+ if (batch.resultDigest) {
378
+ // An uncertain or concurrent write is never submitted twice to this
379
+ // exec. History reconstruction uses provided results as imported data.
380
+ if (batch.resultDigest !== digest || !batch.next?.clean) return fallback("result already submitted or busy")
381
+ return { owner, batch, replay: true, segment: batch.next }
382
+ }
383
+ if (owner.state !== "awaiting-results" || owner.segment !== batch || !batch.clean) return fallback("owner busy")
384
+ return { owner, batch, results, encoded, digest, chat }
385
+ } catch (e) {
386
+ if (!(e instanceof NativeSessionError)) throw e
387
+ return fallback("history incompatible with retained owner")
388
+ }
389
+ }
390
+ // Pending ancestors, new input, and terminal old handles are ordinary
391
+ // imported history, even while a parent Run is still awaiting its results.
392
+ if (chat.messages.some((m) => m?.tool_calls?.some((c) => HANDLE.test(c?.id ?? "")))) return fallback("historical native calls")
393
+ if (selector && this.selectors.has(account + ":" + selector)) return fallback("owner busy")
394
+ return { account, selector, controls, selected: !!selector, chat }
395
+ }
396
+ create(prepared, resolution, wire) {
397
+ if (prepared.fallback || prepared.selected && this.selectors.has(prepared.account + ":" + prepared.selector)) return null
398
+ const crowded = () => this.entries.size >= this.limits.perProcess || [...this.entries].filter((e) => e.account === prepared.account).length >= this.limits.perAccount
399
+ while (crowded()) {
400
+ const accountFull = [...this.entries].filter((e) => e.account === prepared.account).length >= this.limits.perAccount
401
+ const finished = [...this.entries].find((e) => ["complete", "failed", "cancelled", "expired"].includes(e.state) && (!accountFull || e.account === prepared.account))
402
+ if (!finished) return null
403
+ this.forget(finished)
404
+ }
405
+ let owner
406
+ try { owner = new NativeSession(this, prepared, resolution, wire) }
407
+ catch (e) {
408
+ if (!(e instanceof NativeSessionError)) throw e
409
+ return null
410
+ }
411
+ this.entries.add(owner)
412
+ if (prepared.selected) this.selectors.set(owner.account + ":" + owner.selector, owner)
413
+ return owner
414
+ }
415
+ release(owner) {
416
+ const key = owner.account + ":" + owner.selector
417
+ if (this.selectors.get(key) === owner) this.selectors.delete(key)
418
+ }
419
+ forget(owner) {
420
+ owner.accounting?.abandon()
421
+ clearTimeout(owner.replayTimer)
422
+ this.release(owner)
423
+ this.entries.delete(owner)
424
+ for (const [id, h] of this.handles) if (h.owner === owner) this.handles.delete(id)
425
+ owner.blobs.clear()
426
+ owner.checkpoint = null
427
+ owner.execs.clear()
428
+ owner.nativeCalls.clear()
429
+ }
430
+ async clear() {
431
+ await Promise.all([...this.entries].map((e) => e.cancel("native owner removed")))
432
+ for (const e of this.entries) {
433
+ clearTimeout(e.replayTimer)
434
+ this.forget(e)
435
+ }
436
+ }
437
+ }
438
+
439
+ class NativeSession {
440
+ constructor(pool, prepared, resolution, wire) {
441
+ Object.assign(this, prepared)
442
+ this.pool = pool
443
+ this.limits = pool.limits
444
+ this.resolution = resolution
445
+ this.wire = wire
446
+ this.identity = randomBytes(12).toString("hex")
447
+ this.state = "opening"
448
+ this.ac = new AbortController()
449
+ this.execs = new Map()
450
+ this.nativeCalls = new Map()
451
+ this.blobs = new Map()
452
+ this.knownHashes = new Set()
453
+ this.blobSize = 0
454
+ this.retainedBytes = 0
455
+ this.sequence = 0
456
+ this.segments = 0
457
+ this.said = 0
458
+ this.wrote = 0
459
+ this.terminal = false
460
+ this.segment = new Segment(this, prepared.chat)
461
+ this.retain(bytes(prepared.chat))
462
+ this.lifetimeTimer = later(() => this.fail({ status: 410, message: "native Run lifetime expired" }, "expired"), this.limits.lifetime)
463
+ }
464
+ retain(n) {
465
+ if (this.retainedBytes + n > this.limits.outputBytes) invalid("native output/history retention limit reached", 503)
466
+ this.retainedBytes += n
467
+ }
468
+ attach(signal, segment = this.segment) {
469
+ if (signal?.aborted) {
470
+ this.fail({ status: 410, message: "the Chat request was already cancelled" }, "cancelled")
471
+ return
472
+ }
473
+ const abort = () => this.fail({ status: 410, message: "the Chat request was cancelled" }, "cancelled")
474
+ signal?.addEventListener("abort", abort, { once: true })
475
+ segment.detach = () => signal?.removeEventListener("abort", abort)
476
+ }
477
+ delivered(segment = this.segment) {
478
+ if (this.error) return
479
+ segment.clean = true
480
+ segment.detach?.()
481
+ if (this.state === "complete" && segment === this.segment) this.accounting?.delivered()
482
+ }
483
+ closeTransport() {
484
+ if (this.transportClosed) return
485
+ this.transportClosed = true
486
+ clearInterval(this.runHeartbeat)
487
+ clearInterval(this.execHeartbeat)
488
+ clearTimeout(this.batchTimer)
489
+ clearTimeout(this.resultTimer)
490
+ clearTimeout(this.drainTimer)
491
+ clearTimeout(this.lifetimeTimer)
492
+ this.ac.abort()
493
+ this.transport?.close()
494
+ }
495
+ fail(error, state = "failed") {
496
+ if (this.error || this.state === "complete") return
497
+ this.error = error
498
+ this.state = state
499
+ this.segment.fail(error)
500
+ for (const segment of this.batches ?? []) segment.detach?.()
501
+ this.segment.detach?.()
502
+ this.closeTransport()
503
+ // A failed transport has no reusable execs. Forget its handles immediately;
504
+ // subsequent caller-supplied history takes the stateless path.
505
+ this.pool.forget(this)
506
+ }
507
+ async cancel(message) {
508
+ this.fail({ status: 410, message }, "cancelled")
509
+ await this.pumpTask
510
+ }
511
+ bind(transport, blobs, estimate) {
512
+ if (this.transportClosed || this.error) {
513
+ transport.close()
514
+ return
515
+ }
516
+ this.transport = transport
517
+ this.estimate = estimate
518
+ this.blobs = blobs
519
+ this.knownHashes = new Set(blobs.keys())
520
+ this.blobSize = [...blobs.values()].reduce((n, b) => n + b.length, 0)
521
+ if (this.blobSize > this.limits.blobBytes) invalid("native bootstrap blob limit reached", 503)
522
+ if (this.error) return this.closeTransport()
523
+ this.state = "running"
524
+ const { pb } = this.wire
525
+ const heartbeat = (message) => {
526
+ try { this.send(message) } catch (e) { this.fail(failureOf(e)) }
527
+ }
528
+ this.runHeartbeat = setInterval(() => heartbeat(pb().bytes(7, Buffer.alloc(0)).done()), 5000)
529
+ this.execHeartbeat = setInterval(() => {
530
+ for (const c of this.execs.values()) if (!c.submitted) heartbeat(pb().bytes(5, pb().bytes(3, pb().varint(1, c.requestID))).done())
531
+ }, 3000)
532
+ this.runHeartbeat.unref?.()
533
+ this.execHeartbeat.unref?.()
534
+ }
535
+ send(message) {
536
+ if (this.error || this.transportClosed) invalid("native transport is no longer writable", 410)
537
+ this.transport.send(message)
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
+ }
547
+ execClose(id) {
548
+ this.send(this.wire.pb().bytes(5, this.wire.pb().bytes(1, this.wire.pb().varint(1, id))).done())
549
+ }
550
+ encodeResult(message) {
551
+ const { pb } = this.wire
552
+ const success = pb()
553
+ for (const item of resultParts(message, this.limits)) {
554
+ if (item.text !== undefined) success.bytes(1, pb().bytes(1, pb().str(1, item.text)))
555
+ else success.bytes(1, pb().bytes(2, pb().bytes(1, item.data).str(2, item.mimeType)))
556
+ }
557
+ if (message.is_error === true || message.isError === true) success.varint(2, 1)
558
+ return pb().bytes(1, success).done() // McpResult.success, not stored McpToolResult
559
+ }
560
+ resume(prepared, signal) {
561
+ if (prepared.replay) return this.response(prepared.segment, true)
562
+ const batch = prepared.batch
563
+ if (this.error || this.state !== "awaiting-results" || this.segment !== batch || batch.resultDigest) invalid("native continuation is already reserved")
564
+ const next = new Segment(this, prepared.chat)
565
+ this.attach(signal, next)
566
+ if (this.error) invalid(this.error.message, this.error.status)
567
+ // Reserve synchronously. Competing continuations cannot interleave writes.
568
+ batch.resultDigest = prepared.digest
569
+ batch.next = next
570
+ this.segment = next
571
+ this.state = "submitting"
572
+ clearTimeout(this.resultTimer)
573
+ clearTimeout(this.batchTimer)
574
+ try {
575
+ for (let i = 0; i < prepared.results.length; i++) {
576
+ const call = batch.calls.find((c) => c.id === prepared.results[i].tool_call_id)
577
+ const msg = this.wire.pb().varint(1, call.requestID)
578
+ if (call.execID) msg.str(15, call.execID)
579
+ msg.bytes(11, prepared.encoded[i])
580
+ this.resultSubmitted = true
581
+ this.send(this.wire.pb().bytes(2, msg).done())
582
+ call.submitted = true
583
+ this.execClose(call.requestID)
584
+ }
585
+ this.state = "running"
586
+ } catch (e) {
587
+ const error = failureOf(e)
588
+ this.fail({ status: error.status, message: `${error.message}; native result submission failed; its outcome is uncertain and cannot be replayed` })
589
+ }
590
+ return this.response(next)
591
+ }
592
+ response(segment = this.segment, replay = false) {
593
+ return {
594
+ events: segment.iterator({ replay }),
595
+ cancel: () => replay ? Promise.resolve() : this.cancel("the Chat response was cancelled"),
596
+ resolution: this.resolution,
597
+ delivered: () => replay || this.delivered(segment),
598
+ diagnostic: `same-run; bootstrap=imported-history; new-turn=stateless; missing-owner=stateless${replay ? "; replay=true" : ""}`,
599
+ }
600
+ }
601
+ acceptCall(call) {
602
+ if (!Number.isInteger(call.requestID)) invalid("native tool call is missing its exec request ID", 502)
603
+ const prior = this.execs.get(call.requestID)
604
+ const digest = hash(canonical({ requestID: call.requestID, execID: call.execID, nativeID: call.nativeID, name: call.name, args: argsOf(call.args) }))
605
+ if (prior) {
606
+ if (prior.digest !== digest) invalid("native exec identity changed during retransmission", 502)
607
+ return
608
+ }
609
+ // A retransmission of the final exec can arrive after it sealed its batch.
610
+ // Validate that identity first; only a new exec violates the sealed phase.
611
+ if (this.terminal || this.state === "awaiting-results") invalid("native call arrived after its batch was sealed", 502)
612
+ if (call.nativeID && this.nativeCalls.has(call.nativeID)) invalid("native tool-call ID was reused for another exec", 502)
613
+ call.id = `call_cursor_${this.identity}_${this.segments}_${this.segment.calls.length}`
614
+ call.digest = digest
615
+ this.execs.set(call.requestID, call)
616
+ if (call.nativeID) this.nativeCalls.set(call.nativeID, call)
617
+ this.segment.calls.push(call)
618
+ this.retain(bytes(call))
619
+ this.said += call.args.length
620
+ this.waitForBatch()
621
+ this.sealBatch()
622
+ }
623
+ waitForBatch() {
624
+ this.batchTimer ??= later(() => {
625
+ if (this.segment.expected == null && this.segment.calls.length && !this.resultSubmitted) this.missingCount()
626
+ else this.fail({ status: 502, message: "Cursor did not complete its declared native tool batch" })
627
+ }, this.limits.batchWait)
628
+ }
629
+ missingCount() {
630
+ this.fail({ status: 502, message: "Cursor did not declare its native tool batch", retentionIneligible: !this.resultSubmitted })
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
+ }
641
+ listed(count) {
642
+ if (!Number.isSafeInteger(count) || count < 1) invalid("invalid native tool count", 502)
643
+ if (this.segment.expected != null && this.segment.expected !== count) invalid("inconsistent native tool count", 502)
644
+ this.segment.expected = count
645
+ if (this.state === "awaiting-results") return // an identical retransmission
646
+ this.waitForBatch()
647
+ this.sealBatch()
648
+ }
649
+ sealBatch() {
650
+ const segment = this.segment
651
+ if (segment.expected == null) 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
+ }
664
+ clearTimeout(this.batchTimer)
665
+ this.batchTimer = null
666
+ this.state = "awaiting-results"
667
+ this.batches ??= []
668
+ this.batches.push(segment)
669
+ for (const c of segment.calls) this.pool.handles.set(c.id, { owner: this, batch: segment })
670
+ segment.finish({ stop: "tool_calls", usage: {
671
+ prompt_tokens: 0, completion_tokens: 0, total_tokens: 0,
672
+ prompt_tokens_details: { cached_tokens: 0, cache_write_tokens: 0 },
673
+ magpie_native: { accounting: "pending" },
674
+ } })
675
+ this.segments++
676
+ this.resultTimer = later(() => this.fail({ status: 410, message: "native tool-result deadline expired" }, "expired"), this.limits.resultWait)
677
+ }
678
+ store(id, data) {
679
+ if (!id.length) invalid("native blob ID is empty", 502)
680
+ const key = id.toString("hex")
681
+ if (this.knownHashes.has(key) && hash(data) !== key) invalid("native content-addressed bootstrap blob changed", 502)
682
+ const size = this.blobSize - (this.blobs.get(key)?.length ?? 0) + data.length
683
+ if (size + (this.checkpoint?.bytes.length ?? 0) > this.limits.blobBytes) invalid("native blob retention limit reached", 503)
684
+ // Incoming IDs are opaque: a fixed-root ID need not be SHA-256(data).
685
+ this.blobs.set(key, Buffer.from(data))
686
+ this.blobSize = size
687
+ this.validateCheckpoint()
688
+ }
689
+ checkpointReceived(data) {
690
+ if (data.length + this.blobSize > this.limits.blobBytes) invalid("native checkpoint retention limit reached", 503)
691
+ nativeFields(data)
692
+ this.checkpoint = { bytes: Buffer.from(data), sequence: ++this.sequence, status: "incomplete", terminal: this.terminal }
693
+ this.validateCheckpoint()
694
+ }
695
+ validateCheckpoint() {
696
+ if (!this.checkpoint) return
697
+ const state = nativeFields(this.checkpoint.bytes)
698
+ const missing = new Set()
699
+ let supported = true
700
+ const get = (id) => {
701
+ const key = id.toString("hex")
702
+ const b = this.blobs.get(key)
703
+ if (!b) missing.add(key)
704
+ return b
705
+ }
706
+ const refs = new Set([1, 3, 6, 7, 11, 13])
707
+ const known = new Set(Array.from({ length: 34 }, (_, i) => i + 1).filter((n) => n !== 2))
708
+ // These shapes contain additional state/reference contracts which are not
709
+ // implemented by the caller-MCP bridge. Preserve their bytes without
710
+ // claiming reference closure or restart eligibility.
711
+ const complex = new Set([3, 6, 7, 11, 12, 13, 15, 16, 20, 24, 29, 30, 31, 32])
712
+ for (const f of state) {
713
+ if (!known.has(f.num) || complex.has(f.num)) supported = false
714
+ if (refs.has(f.num) && f.data.length) get(f.data)
715
+ if (f.num !== 8) continue
716
+ const turn = get(f.data)
717
+ if (!turn) continue
718
+ const tf = nativeFields(turn)
719
+ const agent = field(tf, 1)
720
+ if (!agent || tf.some((f) => f.num !== 1)) { supported = false; continue }
721
+ const agentFields = nativeFields(agent.data)
722
+ if (!field(agentFields, 1)?.data.length) supported = false
723
+ for (const a of agentFields) {
724
+ if (a.num === 1) {
725
+ const user = get(a.data)
726
+ if (user) nativeFields(user)
727
+ } else if (a.num === 2) {
728
+ const step = get(a.data)
729
+ if (step) {
730
+ const sf = nativeFields(step)
731
+ if (sf.length !== 1 || ![1, 2, 3].includes(sf[0].num)) supported = false
732
+ else {
733
+ const stepFields = nativeFields(sf[0].data)
734
+ if (sf[0].num === 2 && (stepFields.length !== 1 || stepFields[0].num !== 15)) supported = false
735
+ }
736
+ }
737
+ } else if (![3, 4, 5, 6].includes(a.num)) supported = false
738
+ }
739
+ }
740
+ this.checkpoint.missing = missing.size
741
+ this.checkpoint.status = !supported ? "unsupported" : missing.size ? "incomplete" : "complete"
742
+ this.checkpoint.terminal = this.terminal
743
+ }
744
+ terminalSeen(fields) {
745
+ if (this.segment.calls.length) invalid("Cursor ended a turn with unresolved native tool calls", 502)
746
+ this.terminal = true
747
+ this.state = "finalizing"
748
+ this.usageFields = fields
749
+ if (this.checkpoint) this.checkpoint.terminal = true
750
+ clearTimeout(this.batchTimer)
751
+ this.drainTimer = later(() => {
752
+ Promise.resolve().then(() => this.complete()).catch((e) => this.fail(failureOf(e)))
753
+ }, this.limits.drain)
754
+ }
755
+ complete() {
756
+ if (this.completionTask) return this.completionTask
757
+ if (this.error || this.state === "complete") return
758
+ if (!this.terminal) invalid("native Run ended before TurnEnded or a complete final answer", 502)
759
+ const { usageOf } = this.wire
760
+ const measuredInput = this.usageFields?.some((f) => [1, 3, 4].includes(f.num))
761
+ const measuredOutput = this.usageFields?.some((f) => f.num === 2)
762
+ const u = usageOf(this.usageFields ?? [])
763
+ if (!measuredInput) u.input = this.estimate
764
+ if (!measuredOutput) u.output = Math.floor((this.said + 3) / 4)
765
+ const prompt = u.input + u.cacheRead + u.cacheWrite
766
+ this.finalUsage = {
767
+ prompt_tokens: prompt, completion_tokens: u.output, total_tokens: prompt + u.output,
768
+ prompt_tokens_details: { cached_tokens: u.cacheRead, cache_write_tokens: u.cacheWrite },
769
+ ...(this.usageFields?.some((f) => f.num === 5) ? { completion_tokens_details: { reasoning_tokens: u.reasoning } } : {}),
770
+ ...(!measuredInput || !measuredOutput ? { magpie_native: { accounting: "estimated" } } : {}),
771
+ }
772
+ this.state = "finalizing"
773
+ this.closeTransport()
774
+ const finish = (more) => {
775
+ if (this.error) return
776
+ const deferred = more && more.input + more.output + more.cacheRead + more.cacheWrite > 0
777
+ this.segment.finish({ stop: "stop", usage: {
778
+ ...this.finalUsage,
779
+ ...(deferred ? { magpie_deferred: {
780
+ prompt_tokens: more.input + more.cacheRead + more.cacheWrite,
781
+ completion_tokens: more.output,
782
+ cached_tokens: more.cacheRead,
783
+ cache_write_tokens: more.cacheWrite,
784
+ } } : {}),
785
+ } })
786
+ this.state = "complete"
787
+ this.pool.release(this)
788
+ this.replayTimer = later(() => this.pool.forget(this), this.limits.replayWait)
789
+ }
790
+ if (!this.accounting) return finish(null)
791
+ this.completionTask = this.accounting.settle().then(finish)
792
+ return this.completionTask
793
+ }
794
+ async pump(stream) {
795
+ const { pb, exec, failure } = this.wire
796
+ try {
797
+ for await (const f of stream) {
798
+ if (this.error || this.state === "complete") return
799
+ if (f.end) {
800
+ const e = failure(200, f.data)
801
+ if (e.status < 200 || e.status > 299) throw new NativeSessionError(e.status, e.message)
802
+ if (this.segment.calls.length && this.segment.expected == null) { this.missingCount(); return }
803
+ if (!this.terminal && !this.segment.calls.length && this.segment.content) this.terminalSeen([])
804
+ await this.complete()
805
+ return
806
+ }
807
+ for (const m of nativeFields(f.data)) {
808
+ switch (m.num) {
809
+ case 1:
810
+ for (const u of nativeFields(m.data)) {
811
+ const uf = nativeFields(u.data)
812
+ if (u.num === 1 || u.num === 4) {
813
+ const text = str(uf, 1)
814
+ if (this.terminal || this.state === "awaiting-results") invalid("native output arrived outside a running Chat segment", 502)
815
+ if (text) {
816
+ this.said += text.length
817
+ if (u.num === 1) this.wrote += text.length
818
+ this.segment.emit(u.num === 1 ? { text } : { reasoning: text })
819
+ }
820
+ } else if (u.num === 14) {
821
+ if (!this.segment.content && !this.segment.calls.length) invalid("an empty reply (Cursor ended the turn without text or a tool call)", 502)
822
+ this.terminalSeen(uf)
823
+ } else if (u.num === 27) this.listed(num(uf, 1))
824
+ }
825
+ break
826
+ case 2: {
827
+ const c = exec(m.data, this.chatTools, (b) => this.send(b), (id) => this.execClose(id), null, true)
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()
831
+ break
832
+ }
833
+ case 3:
834
+ this.checkpointReceived(m.data)
835
+ break
836
+ case 4: {
837
+ const kv = nativeFields(m.data)
838
+ const id = num(kv, 1)
839
+ for (const k of kv) {
840
+ if (k.num === 2) {
841
+ const want = field(nativeFields(k.data), 1)?.data.toString("hex") ?? ""
842
+ const b = this.blobs.get(want)
843
+ const res = b ? pb().bytes(1, b) : pb().bytes(2, pb().str(1, "blob not found"))
844
+ await this.reply(pb().bytes(3, pb().varint(1, id).bytes(2, res)).done())
845
+ } else if (k.num === 3) {
846
+ try {
847
+ const set = nativeFields(k.data)
848
+ this.store(field(set, 1)?.data ?? Buffer.alloc(0), field(set, 2)?.data ?? Buffer.alloc(0))
849
+ } catch (e) {
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())
851
+ throw e
852
+ }
853
+ await this.reply(pb().bytes(3, pb().varint(1, id).bytes(3, Buffer.alloc(0))).done())
854
+ }
855
+ }
856
+ break
857
+ }
858
+ case 5:
859
+ for (const c of nativeFields(m.data)) if (c.num === 1) {
860
+ const id = num(nativeFields(c.data), 1)
861
+ const pending = this.execs.get(id)
862
+ if (pending && !pending.submitted) invalid("Cursor cancelled a pending native tool exec; late results cannot be submitted", 410)
863
+ }
864
+ break
865
+ case 7:
866
+ invalid("unsupported native interaction query", 502)
867
+ }
868
+ }
869
+ }
870
+ if (!this.error && this.state !== "complete") {
871
+ if (this.terminal) await this.complete()
872
+ else invalid("native reply broke off before completing its turn or tool batch: EOF", 502)
873
+ }
874
+ } catch (e) {
875
+ if (!this.error && this.state !== "complete") this.fail(failureOf(e))
876
+ }
877
+ }
878
+ }
879
+
880
+ export const nativeSessions = new NativeSessionPool()