@playcraft/cli 0.1.85-beta → 0.1.86
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/dist/commands/tools-request.js +24 -0
- package/dist/commands/tools.js +2 -0
- package/dist/utils/agent-api-client.js +57 -18
- package/dist/utils/tool-operation-journal.js +3 -3
- package/dist/workspace-runtime/codex/model-broker.js +82 -269
- package/package.json +3 -4
- package/templates/preview-runtime/v1/manifest.json +1 -1
- package/templates/preview-runtime/v1/runtime.mjs +1 -1
- package/dist/workspace-runtime/codex/capture-isolation.js +0 -10
- package/dist/workspace-runtime/codex/model-capture.js +0 -675
|
@@ -1,675 +0,0 @@
|
|
|
1
|
-
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
-
import { mkdir, writeFile, readFile, readdir, rm, rename, lstat, realpath } from 'node:fs/promises';
|
|
3
|
-
import { join, resolve, relative, isAbsolute } from 'node:path';
|
|
4
|
-
import { z } from 'zod';
|
|
5
|
-
import { platformEndpoint, } from '@playcraft/common/cloud-workspace/runtime';
|
|
6
|
-
import { MODEL_CAPTURE_CHUNK_BYTES, } from '@playcraft/common/model-proxy';
|
|
7
|
-
import { isCaptureIsolationAvailable } from './capture-isolation.js';
|
|
8
|
-
const QUEUE_BYTES = 8 * 1024 * 1024;
|
|
9
|
-
const SPOOL_BYTES = 512 * 1024 * 1024;
|
|
10
|
-
const MAX_CAPTURE_CALLS = 128;
|
|
11
|
-
const manifestSchema = z.object({
|
|
12
|
-
version: z.literal(1),
|
|
13
|
-
identity: z.object({
|
|
14
|
-
modelCallId: z.string().uuid(),
|
|
15
|
-
workspaceId: z.string().uuid(),
|
|
16
|
-
sessionId: z.string().min(1),
|
|
17
|
-
clientRequestId: z.string().min(1),
|
|
18
|
-
directoryName: z.string().min(1).optional(),
|
|
19
|
-
runtimeId: z.string().min(1),
|
|
20
|
-
generation: z.number().int().min(0),
|
|
21
|
-
runtimeEpoch: z.number().int().min(0),
|
|
22
|
-
configId: z.string().uuid(),
|
|
23
|
-
captureCallRowId: z.string().uuid().optional(),
|
|
24
|
-
expiresAt: z.string().datetime(),
|
|
25
|
-
}),
|
|
26
|
-
artifacts: z
|
|
27
|
-
.array(z.object({
|
|
28
|
-
stage: z.enum(['codex_request', 'codex_response']),
|
|
29
|
-
contentType: z.string().max(256),
|
|
30
|
-
representation: z.enum(['raw-http-body', 'decoded_body']),
|
|
31
|
-
parts: z.number().int().min(0).max(128),
|
|
32
|
-
rawBytes: z.number().int().min(0).max(SPOOL_BYTES),
|
|
33
|
-
end: z
|
|
34
|
-
.object({
|
|
35
|
-
sourceEnd: z.enum([
|
|
36
|
-
'eof',
|
|
37
|
-
'aborted',
|
|
38
|
-
'cancelled_by_failover',
|
|
39
|
-
'process_lost',
|
|
40
|
-
'limit_exceeded',
|
|
41
|
-
'io_error',
|
|
42
|
-
'not_started',
|
|
43
|
-
]),
|
|
44
|
-
protocolTerminal: z.enum(['observed', 'absent', 'not_applicable', 'unknown']),
|
|
45
|
-
downstreamFinished: z.boolean().optional(),
|
|
46
|
-
dispatchObserved: z.boolean().optional(),
|
|
47
|
-
responseHeadersObserved: z.boolean().optional(),
|
|
48
|
-
responseStatus: z.number().int().min(100).max(599).optional(),
|
|
49
|
-
upstreamObservedBytes: z.number().int().min(0).optional(),
|
|
50
|
-
downstreamSubmittedBytes: z.number().int().min(0).optional(),
|
|
51
|
-
})
|
|
52
|
-
.optional(),
|
|
53
|
-
}))
|
|
54
|
-
.max(2),
|
|
55
|
-
});
|
|
56
|
-
function within(parent, child) {
|
|
57
|
-
const path = relative(parent, child);
|
|
58
|
-
return !path || (!path.startsWith('..') && !isAbsolute(path));
|
|
59
|
-
}
|
|
60
|
-
let queuedBytes = 0;
|
|
61
|
-
let uploadCount = 0;
|
|
62
|
-
const uploadWaiters = [];
|
|
63
|
-
async function uploadSlot(operation) {
|
|
64
|
-
if (uploadCount >= 2)
|
|
65
|
-
await new Promise((resolve) => uploadWaiters.push(resolve));
|
|
66
|
-
else
|
|
67
|
-
uploadCount++;
|
|
68
|
-
try {
|
|
69
|
-
return await operation();
|
|
70
|
-
}
|
|
71
|
-
finally {
|
|
72
|
-
const next = uploadWaiters.shift();
|
|
73
|
-
if (next)
|
|
74
|
-
next();
|
|
75
|
-
else
|
|
76
|
-
uploadCount--;
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
/** Captures are control-process state. Only immutable scope and raw parts reach disk. */
|
|
80
|
-
export class WorkspaceModelCapture {
|
|
81
|
-
state;
|
|
82
|
-
root;
|
|
83
|
-
diskBytes = 0;
|
|
84
|
-
closed = false;
|
|
85
|
-
ready = false;
|
|
86
|
-
calls = new Set();
|
|
87
|
-
constructor(state) {
|
|
88
|
-
this.state = state;
|
|
89
|
-
this.root = join(state.config.stateDir, 'model-captures');
|
|
90
|
-
}
|
|
91
|
-
async start() {
|
|
92
|
-
if (!isCaptureIsolationAvailable())
|
|
93
|
-
return;
|
|
94
|
-
try {
|
|
95
|
-
await mkdir(this.root, { recursive: true, mode: 0o700 }); // private
|
|
96
|
-
const info = await lstat(this.root);
|
|
97
|
-
const rootPath = await realpath(this.root);
|
|
98
|
-
const workspacePath = await realpath(this.state.config.workspaceRoot).catch(() => resolve(this.state.config.workspaceRoot));
|
|
99
|
-
const codexPath = await realpath(process.env.PLAYCRAFT_CODEX_HOME || '/var/lib/playcraft/codex').catch(() => resolve(process.env.PLAYCRAFT_CODEX_HOME || '/var/lib/playcraft/codex'));
|
|
100
|
-
if (!info.isDirectory() ||
|
|
101
|
-
info.isSymbolicLink() ||
|
|
102
|
-
(info.mode & 0o077) !== 0 ||
|
|
103
|
-
(process.getuid && info.uid !== process.getuid()) ||
|
|
104
|
-
within(workspacePath, rootPath) ||
|
|
105
|
-
within(codexPath, rootPath))
|
|
106
|
-
return;
|
|
107
|
-
this.ready = true;
|
|
108
|
-
for (const name of await readdir(this.root)) {
|
|
109
|
-
if (!z.string().uuid().safeParse(name).success)
|
|
110
|
-
continue;
|
|
111
|
-
const directory = join(this.root, name);
|
|
112
|
-
try {
|
|
113
|
-
const entry = await lstat(directory);
|
|
114
|
-
if (!entry.isDirectory() ||
|
|
115
|
-
entry.isSymbolicLink() ||
|
|
116
|
-
entry.uid !== info.uid ||
|
|
117
|
-
entry.mode & 0o077)
|
|
118
|
-
throw new Error('capture_private_directory_required');
|
|
119
|
-
const manifest = manifestSchema.parse(JSON.parse(await readFile(join(directory, 'manifest.json'), 'utf8')));
|
|
120
|
-
if (manifest.version !== 1 ||
|
|
121
|
-
manifest.identity.modelCallId !== name ||
|
|
122
|
-
manifest.identity.runtimeId !== this.state.config.runtimeId ||
|
|
123
|
-
manifest.identity.generation !== this.state.config.generation ||
|
|
124
|
-
manifest.identity.runtimeEpoch !== this.state.runtimeEpoch ||
|
|
125
|
-
Date.parse(manifest.identity.expiresAt) <= Date.now() ||
|
|
126
|
-
!manifest.identity.captureCallRowId ||
|
|
127
|
-
this.calls.size >= MAX_CAPTURE_CALLS) {
|
|
128
|
-
await rm(directory, { recursive: true, force: true });
|
|
129
|
-
continue;
|
|
130
|
-
}
|
|
131
|
-
let existingBytes = 0;
|
|
132
|
-
const stages = new Set();
|
|
133
|
-
for (const artifact of manifest.artifacts) {
|
|
134
|
-
if (stages.has(artifact.stage))
|
|
135
|
-
throw new Error('capture_duplicate_artifact');
|
|
136
|
-
stages.add(artifact.stage);
|
|
137
|
-
let actualBytes = 0;
|
|
138
|
-
for (let partNo = 0; partNo < artifact.parts; partNo++) {
|
|
139
|
-
const part = await lstat(join(directory, `${artifact.stage}.${partNo}.part`));
|
|
140
|
-
if (!part.isFile() ||
|
|
141
|
-
part.isSymbolicLink() ||
|
|
142
|
-
part.uid !== info.uid ||
|
|
143
|
-
part.mode & 0o077 ||
|
|
144
|
-
part.size > MODEL_CAPTURE_CHUNK_BYTES)
|
|
145
|
-
throw new Error('capture_private_part_required');
|
|
146
|
-
actualBytes += part.size;
|
|
147
|
-
}
|
|
148
|
-
if (actualBytes !== artifact.rawBytes)
|
|
149
|
-
throw new Error('capture_part_manifest_mismatch');
|
|
150
|
-
existingBytes += actualBytes;
|
|
151
|
-
if (!artifact.end)
|
|
152
|
-
artifact.end = {
|
|
153
|
-
sourceEnd: 'process_lost',
|
|
154
|
-
protocolTerminal: 'unknown',
|
|
155
|
-
...(artifact.stage === 'codex_response' ? { downstreamFinished: false } : {}),
|
|
156
|
-
};
|
|
157
|
-
}
|
|
158
|
-
if (this.diskBytes + existingBytes > SPOOL_BYTES)
|
|
159
|
-
throw new Error('capture_spool_limit');
|
|
160
|
-
this.diskBytes += existingBytes;
|
|
161
|
-
const call = new CaptureCall(this, directory, manifest);
|
|
162
|
-
this.calls.add(call);
|
|
163
|
-
void call.upload();
|
|
164
|
-
}
|
|
165
|
-
catch {
|
|
166
|
-
await rm(directory, { recursive: true, force: true }).catch(() => { });
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
catch {
|
|
171
|
-
/* Capture setup failure cannot prevent the loopback broker from starting. */
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
begin(modelCallId, credential) {
|
|
175
|
-
const policy = credential.claims.capturePolicy;
|
|
176
|
-
const origin = credential.claims.traceOrigin;
|
|
177
|
-
if (this.closed ||
|
|
178
|
-
!this.ready ||
|
|
179
|
-
this.calls.size >= MAX_CAPTURE_CALLS ||
|
|
180
|
-
!policy?.enabled ||
|
|
181
|
-
!origin ||
|
|
182
|
-
!('directory' in origin) ||
|
|
183
|
-
Date.parse(policy.expiresAt) <= Date.now())
|
|
184
|
-
return undefined;
|
|
185
|
-
const identity = {
|
|
186
|
-
modelCallId,
|
|
187
|
-
workspaceId: credential.claims.workspaceId,
|
|
188
|
-
runtimeId: credential.claims.runtimeId,
|
|
189
|
-
generation: credential.claims.generation,
|
|
190
|
-
runtimeEpoch: credential.claims.runtimeEpoch,
|
|
191
|
-
sessionId: origin.sessionId,
|
|
192
|
-
directoryName: typeof origin.directory === 'string' ? origin.directory : undefined,
|
|
193
|
-
clientRequestId: origin.clientRequestId,
|
|
194
|
-
configId: credential.configId,
|
|
195
|
-
expiresAt: new Date(Math.min(Date.parse(policy.expiresAt), Date.now() + 3_600_000)).toISOString(),
|
|
196
|
-
};
|
|
197
|
-
const call = new CaptureCall(this, join(this.root, modelCallId), { version: 1, identity, artifacts: [] }, credential.token, {
|
|
198
|
-
request: Math.min(policy.maxRequestBytes, 128 * 1024 * 1024),
|
|
199
|
-
response: Math.min(policy.maxResponseBytes, 512 * 1024 * 1024),
|
|
200
|
-
});
|
|
201
|
-
this.calls.add(call);
|
|
202
|
-
return call;
|
|
203
|
-
}
|
|
204
|
-
reserve(bytes) {
|
|
205
|
-
if (this.diskBytes + bytes > SPOOL_BYTES)
|
|
206
|
-
return false;
|
|
207
|
-
this.diskBytes += bytes;
|
|
208
|
-
return true;
|
|
209
|
-
}
|
|
210
|
-
release(bytes) {
|
|
211
|
-
this.diskBytes = Math.max(0, this.diskBytes - bytes);
|
|
212
|
-
}
|
|
213
|
-
done(call) {
|
|
214
|
-
this.calls.delete(call);
|
|
215
|
-
}
|
|
216
|
-
get runtime() {
|
|
217
|
-
return this.state;
|
|
218
|
-
}
|
|
219
|
-
get supported() {
|
|
220
|
-
return this.ready && !this.closed;
|
|
221
|
-
}
|
|
222
|
-
async close() {
|
|
223
|
-
this.closed = true;
|
|
224
|
-
// Only local writes are drained. Network/storage availability never delays shutdown.
|
|
225
|
-
await Promise.allSettled([...this.calls].map((call) => call.flush()));
|
|
226
|
-
for (const call of this.calls)
|
|
227
|
-
call.close();
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
export class CaptureCall {
|
|
231
|
-
owner;
|
|
232
|
-
directory;
|
|
233
|
-
manifest;
|
|
234
|
-
limits;
|
|
235
|
-
writes = Promise.resolve();
|
|
236
|
-
manifests = Promise.resolve();
|
|
237
|
-
token;
|
|
238
|
-
tokenExpires = 0;
|
|
239
|
-
uploading = false;
|
|
240
|
-
stopped = false;
|
|
241
|
-
reservedBytes = 0;
|
|
242
|
-
removing;
|
|
243
|
-
retry;
|
|
244
|
-
artifacts = new Map();
|
|
245
|
-
nextParts = new Map();
|
|
246
|
-
failedStages = new Set();
|
|
247
|
-
constructor(owner, directory, manifest, token, limits = { request: 128 * 1024 * 1024, response: 512 * 1024 * 1024 }) {
|
|
248
|
-
this.owner = owner;
|
|
249
|
-
this.directory = directory;
|
|
250
|
-
this.manifest = manifest;
|
|
251
|
-
this.limits = limits;
|
|
252
|
-
this.token = token;
|
|
253
|
-
this.reservedBytes = manifest.artifacts.reduce((sum, artifact) => sum + artifact.rawBytes, 0);
|
|
254
|
-
}
|
|
255
|
-
artifact(stage, contentType = 'application/octet-stream', representation = 'raw-http-body') {
|
|
256
|
-
let artifact = this.artifacts.get(stage);
|
|
257
|
-
if (!artifact) {
|
|
258
|
-
const record = { stage, contentType, representation, parts: 0, rawBytes: 0 };
|
|
259
|
-
this.manifest.artifacts.push(record);
|
|
260
|
-
artifact = new CaptureArtifact(this, record, stage === 'codex_request' ? this.limits.request : this.limits.response);
|
|
261
|
-
this.artifacts.set(stage, artifact);
|
|
262
|
-
this.schedule(async () => {
|
|
263
|
-
if (this.stopped)
|
|
264
|
-
return;
|
|
265
|
-
await mkdir(this.directory, { recursive: true, mode: 0o700 }); // private
|
|
266
|
-
await this.persist();
|
|
267
|
-
});
|
|
268
|
-
}
|
|
269
|
-
return artifact;
|
|
270
|
-
}
|
|
271
|
-
schedule(operation) {
|
|
272
|
-
this.writes = this.writes.then(operation).catch(() => {
|
|
273
|
-
this.stopped = true;
|
|
274
|
-
// Run after this chain's rejection handler returns, so cleanup cannot await itself.
|
|
275
|
-
setImmediate(() => {
|
|
276
|
-
void this.remove();
|
|
277
|
-
});
|
|
278
|
-
});
|
|
279
|
-
}
|
|
280
|
-
part(record, bytes) {
|
|
281
|
-
const partNo = this.nextParts.get(record.stage) ?? record.parts;
|
|
282
|
-
this.nextParts.set(record.stage, partNo + 1);
|
|
283
|
-
if (!this.owner.reserve(bytes.length)) {
|
|
284
|
-
throw new Error('capture_spool_limit');
|
|
285
|
-
}
|
|
286
|
-
this.reservedBytes += bytes.length;
|
|
287
|
-
this.schedule(async () => {
|
|
288
|
-
let written = false;
|
|
289
|
-
try {
|
|
290
|
-
if (this.stopped || this.failedStages.has(record.stage)) {
|
|
291
|
-
this.release(bytes.length);
|
|
292
|
-
return;
|
|
293
|
-
}
|
|
294
|
-
await writeFile(this.partPath(record.stage, partNo), bytes, { flag: 'wx', mode: 0o600 }); // private
|
|
295
|
-
written = true;
|
|
296
|
-
record.rawBytes += bytes.length;
|
|
297
|
-
record.parts = partNo + 1;
|
|
298
|
-
await this.persist();
|
|
299
|
-
}
|
|
300
|
-
catch (error) {
|
|
301
|
-
if (!written)
|
|
302
|
-
this.release(bytes.length);
|
|
303
|
-
this.failedStages.add(record.stage);
|
|
304
|
-
}
|
|
305
|
-
finally {
|
|
306
|
-
queuedBytes -= bytes.length;
|
|
307
|
-
}
|
|
308
|
-
});
|
|
309
|
-
}
|
|
310
|
-
finish(record, end) {
|
|
311
|
-
this.schedule(async () => {
|
|
312
|
-
if (this.stopped)
|
|
313
|
-
return;
|
|
314
|
-
record.end = {
|
|
315
|
-
...end,
|
|
316
|
-
sourceEnd: this.failedStages.has(record.stage) ? 'io_error' : end.sourceEnd,
|
|
317
|
-
};
|
|
318
|
-
await this.persist();
|
|
319
|
-
});
|
|
320
|
-
void this.upload();
|
|
321
|
-
}
|
|
322
|
-
flush() {
|
|
323
|
-
return this.writes;
|
|
324
|
-
}
|
|
325
|
-
close() {
|
|
326
|
-
this.stopped = true;
|
|
327
|
-
if (this.retry)
|
|
328
|
-
clearTimeout(this.retry);
|
|
329
|
-
}
|
|
330
|
-
get active() {
|
|
331
|
-
return !this.stopped;
|
|
332
|
-
}
|
|
333
|
-
release(bytes) {
|
|
334
|
-
this.owner.release(bytes);
|
|
335
|
-
this.reservedBytes = Math.max(0, this.reservedBytes - bytes);
|
|
336
|
-
}
|
|
337
|
-
remove() {
|
|
338
|
-
if (this.removing)
|
|
339
|
-
return this.removing;
|
|
340
|
-
this.stopped = true;
|
|
341
|
-
for (const artifact of this.artifacts.values())
|
|
342
|
-
artifact.finish({ sourceEnd: 'io_error', protocolTerminal: 'unknown' });
|
|
343
|
-
if (this.retry)
|
|
344
|
-
clearTimeout(this.retry);
|
|
345
|
-
this.removing = (async () => {
|
|
346
|
-
await this.writes;
|
|
347
|
-
await this.manifests.catch(() => { });
|
|
348
|
-
try {
|
|
349
|
-
await rm(this.directory, { recursive: true, force: true });
|
|
350
|
-
this.release(this.reservedBytes);
|
|
351
|
-
}
|
|
352
|
-
catch {
|
|
353
|
-
/* Keep quota reserved while files cannot be removed. */
|
|
354
|
-
}
|
|
355
|
-
finally {
|
|
356
|
-
this.owner.done(this);
|
|
357
|
-
}
|
|
358
|
-
})();
|
|
359
|
-
return this.removing;
|
|
360
|
-
}
|
|
361
|
-
partPath(stage, part) {
|
|
362
|
-
return join(this.directory, `${stage}.${part}.part`);
|
|
363
|
-
}
|
|
364
|
-
persist() {
|
|
365
|
-
// Registration and body writes share one manifest writer, including snapshot creation.
|
|
366
|
-
const next = this.manifests
|
|
367
|
-
.catch(() => { })
|
|
368
|
-
.then(async () => {
|
|
369
|
-
const temporary = join(this.directory, `${randomUUID()}.tmp`);
|
|
370
|
-
await writeFile(temporary, JSON.stringify(this.manifest), { mode: 0o600 }); // private
|
|
371
|
-
await rename(temporary, join(this.directory, 'manifest.json'));
|
|
372
|
-
});
|
|
373
|
-
this.manifests = next;
|
|
374
|
-
return next;
|
|
375
|
-
}
|
|
376
|
-
async authorize() {
|
|
377
|
-
if (this.token && (!this.tokenExpires || this.tokenExpires > Date.now() + 10_000))
|
|
378
|
-
return this.token;
|
|
379
|
-
const config = this.owner.runtime.config;
|
|
380
|
-
const response = await fetch(platformEndpoint(config.publicBaseUrl, '/runtime/internal/model-capture-upload-grants'), {
|
|
381
|
-
method: 'POST',
|
|
382
|
-
redirect: 'error',
|
|
383
|
-
signal: AbortSignal.timeout(10_000),
|
|
384
|
-
headers: {
|
|
385
|
-
authorization: `Bearer ${config.controlToken}`,
|
|
386
|
-
'content-type': 'application/json',
|
|
387
|
-
'playcraft-env': config.environment,
|
|
388
|
-
},
|
|
389
|
-
body: JSON.stringify({
|
|
390
|
-
runtimeId: this.manifest.identity.runtimeId,
|
|
391
|
-
generation: this.manifest.identity.generation,
|
|
392
|
-
runtimeEpoch: this.manifest.identity.runtimeEpoch,
|
|
393
|
-
workspaceId: this.manifest.identity.workspaceId,
|
|
394
|
-
sessionId: this.manifest.identity.sessionId,
|
|
395
|
-
directoryName: this.manifest.identity.directoryName,
|
|
396
|
-
clientRequestId: this.manifest.identity.clientRequestId,
|
|
397
|
-
modelCallId: this.manifest.identity.modelCallId,
|
|
398
|
-
captureCallRowId: this.manifest.identity.captureCallRowId,
|
|
399
|
-
}),
|
|
400
|
-
});
|
|
401
|
-
if (!response.ok) {
|
|
402
|
-
await response.body?.cancel();
|
|
403
|
-
if ([401, 403, 404, 410].includes(response.status))
|
|
404
|
-
await this.remove();
|
|
405
|
-
throw new Error('capture_authority_unavailable');
|
|
406
|
-
}
|
|
407
|
-
const grant = z
|
|
408
|
-
.object({ token: z.string().min(1), expiresAt: z.string().datetime() })
|
|
409
|
-
.parse(await response.json());
|
|
410
|
-
this.token = grant.token;
|
|
411
|
-
this.tokenExpires = Date.parse(grant.expiresAt);
|
|
412
|
-
return grant.token;
|
|
413
|
-
}
|
|
414
|
-
async send(path, init) {
|
|
415
|
-
const token = await this.authorize();
|
|
416
|
-
const base = `/api/llm-proxy/workspace-captures/${this.manifest.identity.modelCallId}`;
|
|
417
|
-
const response = await fetch(platformEndpoint(this.owner.runtime.config.publicBaseUrl, `${base}${path}`), {
|
|
418
|
-
...init,
|
|
419
|
-
redirect: 'error',
|
|
420
|
-
signal: AbortSignal.timeout(15_000),
|
|
421
|
-
headers: { ...init.headers, authorization: `Bearer ${token}` },
|
|
422
|
-
});
|
|
423
|
-
if (!response.ok) {
|
|
424
|
-
const terminal = response.status === 410 ||
|
|
425
|
-
((response.status === 401 || response.status === 403) && this.tokenExpires > 0);
|
|
426
|
-
if (response.status === 401 || response.status === 403) {
|
|
427
|
-
this.token = undefined;
|
|
428
|
-
this.tokenExpires = 0;
|
|
429
|
-
}
|
|
430
|
-
await response.body?.cancel();
|
|
431
|
-
if (terminal)
|
|
432
|
-
await this.remove();
|
|
433
|
-
throw new Error('capture_upload_unavailable');
|
|
434
|
-
}
|
|
435
|
-
return response;
|
|
436
|
-
}
|
|
437
|
-
verifyArchive(record, sha256, remote) {
|
|
438
|
-
if (remote.stage !== record.stage ||
|
|
439
|
-
remote.archiveState !== 'archived' ||
|
|
440
|
-
remote.rawBytes !== record.rawBytes ||
|
|
441
|
-
remote.partCount !== record.parts ||
|
|
442
|
-
remote.sha256 !== sha256 ||
|
|
443
|
-
Object.entries(record.end ?? {}).some(([key, value]) => remote[key] !== value)) {
|
|
444
|
-
throw new Error('capture_archive_confirmation_mismatch');
|
|
445
|
-
}
|
|
446
|
-
}
|
|
447
|
-
async upload() {
|
|
448
|
-
if (this.uploading || this.stopped)
|
|
449
|
-
return;
|
|
450
|
-
this.uploading = true;
|
|
451
|
-
try {
|
|
452
|
-
await this.writes;
|
|
453
|
-
if (this.stopped)
|
|
454
|
-
return;
|
|
455
|
-
if (Date.parse(this.manifest.identity.expiresAt) <= Date.now()) {
|
|
456
|
-
await this.remove();
|
|
457
|
-
return;
|
|
458
|
-
}
|
|
459
|
-
await uploadSlot(async () => {
|
|
460
|
-
if (!this.manifest.identity.captureCallRowId) {
|
|
461
|
-
const response = await this.send('/register', {
|
|
462
|
-
method: 'POST',
|
|
463
|
-
headers: { 'content-type': 'application/json' },
|
|
464
|
-
body: JSON.stringify({ configId: this.manifest.identity.configId }),
|
|
465
|
-
});
|
|
466
|
-
const registration = (await response.json());
|
|
467
|
-
if (registration.capability !== 'available' || !registration.call) {
|
|
468
|
-
await this.remove();
|
|
469
|
-
return;
|
|
470
|
-
}
|
|
471
|
-
this.manifest.identity.captureCallRowId = registration.call.callRowId;
|
|
472
|
-
await this.persist();
|
|
473
|
-
}
|
|
474
|
-
const statusResponse = await this.send('/upload-status', { method: 'GET' });
|
|
475
|
-
const status = (await statusResponse.json());
|
|
476
|
-
if (status.call?.callRowId !== this.manifest.identity.captureCallRowId ||
|
|
477
|
-
status.call.modelCallId !== this.manifest.identity.modelCallId ||
|
|
478
|
-
!Array.isArray(status.artifacts)) {
|
|
479
|
-
throw new Error('capture_status_identity_mismatch');
|
|
480
|
-
}
|
|
481
|
-
for (const record of this.manifest.artifacts) {
|
|
482
|
-
if (!record.end)
|
|
483
|
-
continue;
|
|
484
|
-
const remote = status.artifacts.find((item) => item.stage === record.stage);
|
|
485
|
-
const confirmed = new Map((remote?.confirmedParts ?? []).map((part) => [part.partNo, part]));
|
|
486
|
-
const hash = createHash('sha256');
|
|
487
|
-
for (let partNo = 0; partNo < record.parts; partNo++) {
|
|
488
|
-
const bytes = await readFile(this.partPath(record.stage, partNo));
|
|
489
|
-
hash.update(bytes);
|
|
490
|
-
const sha256 = createHash('sha256').update(bytes).digest('hex');
|
|
491
|
-
const part = confirmed.get(partNo);
|
|
492
|
-
if (part) {
|
|
493
|
-
if (part.sha256 !== sha256 || part.rawBytes !== bytes.length)
|
|
494
|
-
throw new Error('capture_part_confirmation_mismatch');
|
|
495
|
-
continue;
|
|
496
|
-
}
|
|
497
|
-
const response = await this.send(`/artifacts/${record.stage}/chunks/${partNo}`, {
|
|
498
|
-
method: 'PUT',
|
|
499
|
-
headers: {
|
|
500
|
-
'content-type': 'application/octet-stream',
|
|
501
|
-
'x-capture-sha256': sha256,
|
|
502
|
-
'x-capture-content-type': record.contentType,
|
|
503
|
-
'x-capture-representation': record.representation,
|
|
504
|
-
},
|
|
505
|
-
body: bytes,
|
|
506
|
-
});
|
|
507
|
-
await response.body?.cancel();
|
|
508
|
-
}
|
|
509
|
-
const sha256 = hash.digest('hex');
|
|
510
|
-
if (remote?.archiveState === 'archived') {
|
|
511
|
-
this.verifyArchive(record, sha256, remote);
|
|
512
|
-
continue;
|
|
513
|
-
}
|
|
514
|
-
const response = await this.send(`/artifacts/${record.stage}/finalize`, {
|
|
515
|
-
method: 'POST',
|
|
516
|
-
headers: { 'content-type': 'application/json' },
|
|
517
|
-
body: JSON.stringify({
|
|
518
|
-
...record.end,
|
|
519
|
-
partCount: record.parts,
|
|
520
|
-
rawBytes: record.rawBytes,
|
|
521
|
-
sha256,
|
|
522
|
-
}),
|
|
523
|
-
});
|
|
524
|
-
this.verifyArchive(record, sha256, (await response.json()));
|
|
525
|
-
}
|
|
526
|
-
});
|
|
527
|
-
if ((this.manifest.artifacts.length === 2 || this.artifacts.size === 0) &&
|
|
528
|
-
this.manifest.artifacts.every((item) => item.end)) {
|
|
529
|
-
await this.remove();
|
|
530
|
-
return;
|
|
531
|
-
}
|
|
532
|
-
}
|
|
533
|
-
catch {
|
|
534
|
-
/* Retry only archival transport, never model execution. */
|
|
535
|
-
}
|
|
536
|
-
finally {
|
|
537
|
-
this.uploading = false;
|
|
538
|
-
if (!this.stopped) {
|
|
539
|
-
this.retry = setTimeout(() => {
|
|
540
|
-
void this.upload();
|
|
541
|
-
}, 5_000);
|
|
542
|
-
this.retry.unref();
|
|
543
|
-
}
|
|
544
|
-
}
|
|
545
|
-
}
|
|
546
|
-
}
|
|
547
|
-
export class CaptureArtifact {
|
|
548
|
-
call;
|
|
549
|
-
record;
|
|
550
|
-
limit;
|
|
551
|
-
pending = [];
|
|
552
|
-
pendingBytes = 0;
|
|
553
|
-
receivedBytes = 0;
|
|
554
|
-
ended = false;
|
|
555
|
-
failure;
|
|
556
|
-
observedBytes = 0;
|
|
557
|
-
submittedBytes = 0;
|
|
558
|
-
constructor(call, record, limit) {
|
|
559
|
-
this.call = call;
|
|
560
|
-
this.record = record;
|
|
561
|
-
this.limit = limit;
|
|
562
|
-
}
|
|
563
|
-
contentType(value) {
|
|
564
|
-
this.record.contentType = value.slice(0, 256);
|
|
565
|
-
}
|
|
566
|
-
upstreamObserved(bytes) {
|
|
567
|
-
this.observedBytes += bytes;
|
|
568
|
-
}
|
|
569
|
-
append(bytes) {
|
|
570
|
-
if (this.ended || !bytes.byteLength)
|
|
571
|
-
return;
|
|
572
|
-
this.submittedBytes += bytes.byteLength;
|
|
573
|
-
if (this.failure || !this.call.active)
|
|
574
|
-
return;
|
|
575
|
-
if (this.receivedBytes + bytes.byteLength > this.limit ||
|
|
576
|
-
queuedBytes + bytes.byteLength > QUEUE_BYTES) {
|
|
577
|
-
this.failure = 'limit_exceeded';
|
|
578
|
-
return;
|
|
579
|
-
}
|
|
580
|
-
this.receivedBytes += bytes.byteLength;
|
|
581
|
-
queuedBytes += bytes.byteLength;
|
|
582
|
-
let offset = 0;
|
|
583
|
-
while (offset < bytes.byteLength) {
|
|
584
|
-
const length = Math.min(MODEL_CAPTURE_CHUNK_BYTES - this.pendingBytes, bytes.byteLength - offset);
|
|
585
|
-
this.pending.push(Buffer.from(bytes.subarray(offset, offset + length)));
|
|
586
|
-
this.pendingBytes += length;
|
|
587
|
-
offset += length;
|
|
588
|
-
if (this.pendingBytes === MODEL_CAPTURE_CHUNK_BYTES)
|
|
589
|
-
this.flushPart();
|
|
590
|
-
}
|
|
591
|
-
}
|
|
592
|
-
finish(end) {
|
|
593
|
-
if (this.ended)
|
|
594
|
-
return;
|
|
595
|
-
this.ended = true;
|
|
596
|
-
if (!this.call.active) {
|
|
597
|
-
queuedBytes -= this.pendingBytes;
|
|
598
|
-
this.pending = [];
|
|
599
|
-
this.pendingBytes = 0;
|
|
600
|
-
return;
|
|
601
|
-
}
|
|
602
|
-
this.flushPart();
|
|
603
|
-
this.call.finish(this.record, {
|
|
604
|
-
...end,
|
|
605
|
-
sourceEnd: this.failure ?? end.sourceEnd,
|
|
606
|
-
...(this.record.stage === 'codex_response'
|
|
607
|
-
? {
|
|
608
|
-
upstreamObservedBytes: this.observedBytes,
|
|
609
|
-
downstreamSubmittedBytes: this.submittedBytes,
|
|
610
|
-
}
|
|
611
|
-
: {}),
|
|
612
|
-
});
|
|
613
|
-
}
|
|
614
|
-
flushPart() {
|
|
615
|
-
if (!this.pendingBytes)
|
|
616
|
-
return;
|
|
617
|
-
const bytes = Buffer.concat(this.pending, this.pendingBytes);
|
|
618
|
-
this.pending = [];
|
|
619
|
-
this.pendingBytes = 0;
|
|
620
|
-
try {
|
|
621
|
-
this.call.part(this.record, bytes);
|
|
622
|
-
}
|
|
623
|
-
catch {
|
|
624
|
-
queuedBytes -= bytes.length;
|
|
625
|
-
this.failure = 'limit_exceeded';
|
|
626
|
-
}
|
|
627
|
-
}
|
|
628
|
-
}
|
|
629
|
-
/** Observes Node's accepted bytes without changing the return value or drain handling. */
|
|
630
|
-
export function captureSubmittedResponse(response, artifact) {
|
|
631
|
-
const append = (chunk, encoding) => {
|
|
632
|
-
try {
|
|
633
|
-
if (typeof chunk === 'string')
|
|
634
|
-
artifact.append(Buffer.from(chunk, typeof encoding === 'string' ? encoding : undefined));
|
|
635
|
-
else if (chunk instanceof Uint8Array)
|
|
636
|
-
artifact.append(chunk);
|
|
637
|
-
}
|
|
638
|
-
catch {
|
|
639
|
-
/* Archival must never turn a successful write into an inference failure. */
|
|
640
|
-
}
|
|
641
|
-
};
|
|
642
|
-
const write = response.write;
|
|
643
|
-
response.write = function (...args) {
|
|
644
|
-
const writable = !this.destroyed && !this.writableEnded && !this.socket?.destroyed;
|
|
645
|
-
const accepted = Reflect.apply(write, this, args);
|
|
646
|
-
if (writable && !this.destroyed && !this.socket?.destroyed)
|
|
647
|
-
append(args[0], args[1]);
|
|
648
|
-
return accepted;
|
|
649
|
-
};
|
|
650
|
-
const end = response.end;
|
|
651
|
-
response.end = function (chunk, encoding, callback) {
|
|
652
|
-
const writable = !this.destroyed && !this.writableEnded && !this.socket?.destroyed;
|
|
653
|
-
const accepted = Reflect.apply(end, this, [chunk, encoding, callback]);
|
|
654
|
-
if (writable && !this.destroyed && !this.socket?.destroyed)
|
|
655
|
-
append(chunk, encoding);
|
|
656
|
-
return accepted;
|
|
657
|
-
};
|
|
658
|
-
response.once('finish', () => queueMicrotask(() => {
|
|
659
|
-
artifact.contentType(String(response.getHeader('content-type') ?? 'application/octet-stream'));
|
|
660
|
-
artifact.finish({
|
|
661
|
-
sourceEnd: 'eof',
|
|
662
|
-
protocolTerminal: /event-stream|ndjson/i.test(String(response.getHeader('content-type') ?? ''))
|
|
663
|
-
? 'unknown'
|
|
664
|
-
: 'not_applicable',
|
|
665
|
-
downstreamFinished: true,
|
|
666
|
-
responseStatus: response.statusCode,
|
|
667
|
-
});
|
|
668
|
-
}));
|
|
669
|
-
response.once('close', () => queueMicrotask(() => artifact.finish({
|
|
670
|
-
sourceEnd: response.writableFinished ? 'eof' : 'aborted',
|
|
671
|
-
protocolTerminal: 'unknown',
|
|
672
|
-
downstreamFinished: response.writableFinished,
|
|
673
|
-
responseStatus: response.statusCode,
|
|
674
|
-
})));
|
|
675
|
-
}
|