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