@playcraft/cli 0.1.85-beta → 0.1.87
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-generation.js +102 -46
- package/dist/commands/tools-request.js +24 -0
- package/dist/commands/tools-utils.js +15 -17
- package/dist/commands/tools.js +2 -0
- package/dist/utils/agent-api-client.js +57 -18
- package/dist/utils/image-asset-context.js +92 -42
- package/dist/utils/tool-operation-journal.js +3 -3
- package/dist/workspace-runtime/codex/config-toml.js +3 -2
- package/dist/workspace-runtime/codex/model-broker-diagnostics.js +149 -0
- package/dist/workspace-runtime/codex/model-broker-request.js +59 -0
- package/dist/workspace-runtime/codex/model-broker.js +221 -84
- package/dist/workspace-runtime/codex/model-capture.js +59 -10
- package/package.json +3 -3
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createHash, randomBytes, randomUUID, timingSafeEqual } from 'node:crypto';
|
|
2
2
|
import { createServer } from 'node:http';
|
|
3
3
|
import { Readable, Transform } from 'node:stream';
|
|
4
|
-
import { pipeline } from 'node:stream/promises';
|
|
4
|
+
import { finished, pipeline } from 'node:stream/promises';
|
|
5
5
|
import { basename, dirname } from 'node:path';
|
|
6
6
|
import { z } from 'zod';
|
|
7
7
|
import { workspaceContextFromRoot } from '../workspaces/registry.js';
|
|
@@ -10,6 +10,8 @@ import { WorkspaceGenerationProvenanceBroker } from './generation-provenance-bro
|
|
|
10
10
|
import { WorkspaceRemixReferencesBroker } from './remix-references-broker.js';
|
|
11
11
|
import { WorkspaceExecutionContextBroker } from './execution-context-broker.js';
|
|
12
12
|
import { WorkspaceSkillDiscoveryBroker } from './skill-discovery-broker.js';
|
|
13
|
+
import { ModelBrokerDiagnostics } from './model-broker-diagnostics.js';
|
|
14
|
+
import { MODEL_REQUEST_BUDGET_MS, ModelBrokerRequestError, modelBrokerFailure, readModelRequestBody, } from './model-broker-request.js';
|
|
13
15
|
import { WorkspaceModelCapture, captureSubmittedResponse, } from './model-capture.js';
|
|
14
16
|
const credentialSchema = z.object({
|
|
15
17
|
token: z.string().min(1),
|
|
@@ -42,6 +44,7 @@ const MAX_MODEL_REQUEST_BYTES = 128 * 1024 * 1024;
|
|
|
42
44
|
/** Keeps renewable execution credentials in control memory, outside Codex and project files. */
|
|
43
45
|
export class WorkspaceModelBroker {
|
|
44
46
|
state;
|
|
47
|
+
requestBudgetMs;
|
|
45
48
|
server;
|
|
46
49
|
baseUrl = '';
|
|
47
50
|
entries = new Map();
|
|
@@ -54,8 +57,9 @@ export class WorkspaceModelBroker {
|
|
|
54
57
|
executionContext;
|
|
55
58
|
skills;
|
|
56
59
|
capture;
|
|
57
|
-
constructor(state) {
|
|
60
|
+
constructor(state, requestBudgetMs = MODEL_REQUEST_BUDGET_MS) {
|
|
58
61
|
this.state = state;
|
|
62
|
+
this.requestBudgetMs = requestBudgetMs;
|
|
59
63
|
this.generation = new WorkspaceGenerationProvenanceBroker(state);
|
|
60
64
|
this.references = new WorkspaceRemixReferencesBroker(state);
|
|
61
65
|
this.executionContext = new WorkspaceExecutionContextBroker(state);
|
|
@@ -174,8 +178,13 @@ export class WorkspaceModelBroker {
|
|
|
174
178
|
base_url: `${this.baseUrl}/${routeKey}`,
|
|
175
179
|
wire_api: 'responses',
|
|
176
180
|
requires_openai_auth: false,
|
|
181
|
+
request_max_retries: 0,
|
|
182
|
+
stream_max_retries: 0,
|
|
183
|
+
stream_idle_timeout_ms: MODEL_REQUEST_BUDGET_MS,
|
|
184
|
+
supports_websockets: false,
|
|
177
185
|
http_headers: { Authorization: `Bearer ${route.capability}` },
|
|
178
186
|
},
|
|
187
|
+
'features.unbounded_connection_retries': false,
|
|
179
188
|
},
|
|
180
189
|
};
|
|
181
190
|
}
|
|
@@ -201,13 +210,17 @@ export class WorkspaceModelBroker {
|
|
|
201
210
|
entry.codexDispatchedAt = new Date().toISOString();
|
|
202
211
|
entry.codexDispatchMark = performance.now();
|
|
203
212
|
}
|
|
204
|
-
async credential(entry, refresh = false) {
|
|
213
|
+
async credential(entry, refresh = false, signal) {
|
|
214
|
+
signal?.throwIfAborted();
|
|
205
215
|
if (!this.valid(entry))
|
|
206
216
|
throw new Error('model_access_denied');
|
|
207
217
|
if (!refresh &&
|
|
208
218
|
entry.credential &&
|
|
209
219
|
Date.parse(entry.credential.expiresAt) > Date.now() + 60_000)
|
|
210
220
|
return entry.credential;
|
|
221
|
+
// A model request owns its renewal cancellation; it must not abort another caller's renewal.
|
|
222
|
+
if (signal)
|
|
223
|
+
return this.renew(entry, signal);
|
|
211
224
|
let pending = this.pending.get(entry);
|
|
212
225
|
if (!pending) {
|
|
213
226
|
pending = this.renew(entry).finally(() => this.pending.delete(entry));
|
|
@@ -215,56 +228,76 @@ export class WorkspaceModelBroker {
|
|
|
215
228
|
}
|
|
216
229
|
return pending;
|
|
217
230
|
}
|
|
218
|
-
async renew(entry) {
|
|
231
|
+
async renew(entry, signal) {
|
|
219
232
|
const config = this.state.config;
|
|
220
233
|
const endpoint = platformEndpoint(config.publicBaseUrl, PLATFORM_ROUTES.modelCredentials);
|
|
221
|
-
const
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
234
|
+
const timeout = new AbortController();
|
|
235
|
+
const timer = setTimeout(() => timeout.abort(new ModelBrokerRequestError(504, 'model_credential_timeout')), 10_000);
|
|
236
|
+
timer.unref();
|
|
237
|
+
try {
|
|
238
|
+
const response = await fetch(endpoint, {
|
|
239
|
+
method: 'POST',
|
|
240
|
+
redirect: 'error',
|
|
241
|
+
signal: signal ? AbortSignal.any([signal, timeout.signal]) : timeout.signal,
|
|
242
|
+
headers: {
|
|
243
|
+
authorization: `Bearer ${config.controlToken}`,
|
|
244
|
+
'content-type': 'application/json',
|
|
245
|
+
'playcraft-env': config.environment,
|
|
246
|
+
...(this.capture.supported ? { 'x-playcraft-model-capture-protocol': '1' } : {}),
|
|
247
|
+
},
|
|
248
|
+
body: JSON.stringify({
|
|
249
|
+
runtimeId: config.runtimeId,
|
|
250
|
+
generation: config.generation,
|
|
251
|
+
runtimeEpoch: entry.epoch,
|
|
252
|
+
workspaceId: entry.workspaceId,
|
|
253
|
+
...(entry.sessionId ? { sessionId: entry.sessionId } : {}),
|
|
254
|
+
...(entry.clientRequestId ? { clientRequestId: entry.clientRequestId } : {}),
|
|
255
|
+
...(entry.modelConfigId ? { modelConfigId: entry.modelConfigId } : {}),
|
|
256
|
+
...(entry.reasoningEffort ? { reasoningEffort: entry.reasoningEffort } : {}),
|
|
257
|
+
}),
|
|
258
|
+
});
|
|
259
|
+
if (!response.ok) {
|
|
260
|
+
const error = (await response.json().catch(() => ({})));
|
|
261
|
+
signal?.throwIfAborted();
|
|
262
|
+
timeout.signal.throwIfAborted();
|
|
263
|
+
throw new ModelBrokerRequestError(response.status, [
|
|
264
|
+
'model_config_required',
|
|
265
|
+
'model_config_denied',
|
|
266
|
+
'reasoning_effort_denied',
|
|
267
|
+
'reasoning_effort_unsupported',
|
|
268
|
+
].includes(error.reason ?? '')
|
|
269
|
+
? error.reason
|
|
270
|
+
: 'model_credential_unavailable');
|
|
271
|
+
}
|
|
272
|
+
const parsed = credentialSchema.safeParse(await response.json());
|
|
273
|
+
if (!parsed.success)
|
|
274
|
+
throw new ModelBrokerRequestError(502, 'model_credential_invalid');
|
|
275
|
+
const credential = parsed.data;
|
|
276
|
+
const claims = credential.claims;
|
|
277
|
+
if (!this.valid(entry) ||
|
|
278
|
+
claims.workspaceId !== entry.workspaceId ||
|
|
279
|
+
claims.runtimeId !== config.runtimeId ||
|
|
280
|
+
claims.userId !== config.userId ||
|
|
281
|
+
claims.environment !== config.environment ||
|
|
282
|
+
claims.generation !== config.generation ||
|
|
283
|
+
claims.runtimeEpoch !== entry.epoch ||
|
|
284
|
+
(entry.modelConfigId !== undefined && credential.configId !== entry.modelConfigId) ||
|
|
285
|
+
Date.parse(credential.expiresAt) <= Date.now() + 1000)
|
|
286
|
+
throw new ModelBrokerRequestError(403, 'model_credential_scope_mismatch');
|
|
287
|
+
entry.modelConfigId = credential.configId;
|
|
288
|
+
entry.credential = credential;
|
|
289
|
+
return credential;
|
|
290
|
+
}
|
|
291
|
+
catch (error) {
|
|
292
|
+
signal?.throwIfAborted();
|
|
293
|
+
timeout.signal.throwIfAborted();
|
|
294
|
+
if (error instanceof ModelBrokerRequestError)
|
|
295
|
+
throw error;
|
|
296
|
+
throw new ModelBrokerRequestError(503, 'model_credential_unavailable', error);
|
|
297
|
+
}
|
|
298
|
+
finally {
|
|
299
|
+
clearTimeout(timer);
|
|
252
300
|
}
|
|
253
|
-
const credential = credentialSchema.parse(await response.json());
|
|
254
|
-
const claims = credential.claims;
|
|
255
|
-
if (!this.valid(entry) ||
|
|
256
|
-
claims.workspaceId !== entry.workspaceId ||
|
|
257
|
-
claims.runtimeId !== config.runtimeId ||
|
|
258
|
-
claims.userId !== config.userId ||
|
|
259
|
-
claims.environment !== config.environment ||
|
|
260
|
-
claims.generation !== config.generation ||
|
|
261
|
-
claims.runtimeEpoch !== entry.epoch ||
|
|
262
|
-
(entry.modelConfigId !== undefined && credential.configId !== entry.modelConfigId) ||
|
|
263
|
-
Date.parse(credential.expiresAt) <= Date.now() + 1000)
|
|
264
|
-
throw new Error('model_credential_scope_mismatch');
|
|
265
|
-
entry.modelConfigId = credential.configId;
|
|
266
|
-
entry.credential = credential;
|
|
267
|
-
return credential;
|
|
268
301
|
}
|
|
269
302
|
async forward(request, response) {
|
|
270
303
|
const receivedMark = performance.now();
|
|
@@ -276,7 +309,16 @@ export class WorkspaceModelBroker {
|
|
|
276
309
|
let capturedRequest;
|
|
277
310
|
let capturedResponse;
|
|
278
311
|
let requestEnded = false;
|
|
279
|
-
|
|
312
|
+
let diagnostics;
|
|
313
|
+
let failure;
|
|
314
|
+
let deadlineTimer;
|
|
315
|
+
let credentialTimer;
|
|
316
|
+
const cancel = () => {
|
|
317
|
+
if (response.writableFinished)
|
|
318
|
+
return;
|
|
319
|
+
diagnostics?.abort('downstream_closed');
|
|
320
|
+
abort.abort();
|
|
321
|
+
};
|
|
280
322
|
response.once('close', cancel);
|
|
281
323
|
try {
|
|
282
324
|
const match = /^\/([0-9a-f-]{36}(?:\/[0-9a-f]{32})?)\/(responses|generation-provenance|remix-references|execution-context|skill-discovery\/(?:match|selection|artifact|activate))$/.exec(request.url ?? '');
|
|
@@ -344,6 +386,32 @@ export class WorkspaceModelBroker {
|
|
|
344
386
|
await this.skills.forward(request, response, selected.feedback, () => this.valid(entry) && route?.selected === selected, action);
|
|
345
387
|
return;
|
|
346
388
|
}
|
|
389
|
+
const modelCallId = randomUUID();
|
|
390
|
+
diagnostics = new ModelBrokerDiagnostics({
|
|
391
|
+
environment: this.state.config.environment,
|
|
392
|
+
runtimeId: this.state.config.runtimeId,
|
|
393
|
+
generation: this.state.config.generation,
|
|
394
|
+
runtimeEpoch: entry.epoch,
|
|
395
|
+
workspaceId: entry.workspaceId,
|
|
396
|
+
sessionId: entry.sessionId,
|
|
397
|
+
clientRequestId: entry.clientRequestId,
|
|
398
|
+
configId: entry.modelConfigId,
|
|
399
|
+
modelCallId,
|
|
400
|
+
requestBudgetMs: this.requestBudgetMs,
|
|
401
|
+
});
|
|
402
|
+
const expire = () => {
|
|
403
|
+
if (abort.signal.aborted)
|
|
404
|
+
return;
|
|
405
|
+
diagnostics?.abort('deadline_exceeded');
|
|
406
|
+
abort.abort(new ModelBrokerRequestError(504, 'model_request_deadline_exceeded'));
|
|
407
|
+
};
|
|
408
|
+
const checkDeadline = () => {
|
|
409
|
+
if (performance.now() - receivedMark >= this.requestBudgetMs)
|
|
410
|
+
expire();
|
|
411
|
+
abort.signal.throwIfAborted();
|
|
412
|
+
};
|
|
413
|
+
deadlineTimer = setTimeout(expire, Math.max(0, this.requestBudgetMs - (performance.now() - receivedMark)));
|
|
414
|
+
deadlineTimer.unref();
|
|
347
415
|
const rejectOversizedBody = () => {
|
|
348
416
|
response.writeHead(413, {
|
|
349
417
|
'content-type': 'application/json',
|
|
@@ -370,36 +438,52 @@ export class WorkspaceModelBroker {
|
|
|
370
438
|
active.add(abort);
|
|
371
439
|
this.sessionRequests.set(route, active);
|
|
372
440
|
const credentialMark = performance.now();
|
|
373
|
-
const credential = await this.credential(entry);
|
|
441
|
+
const credential = await this.credential(entry, false, abort.signal);
|
|
442
|
+
checkDeadline();
|
|
374
443
|
const credentialWaitMs = elapsed(credentialMark);
|
|
375
|
-
const
|
|
376
|
-
const
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
? this.capture.begin(modelCallId, credential)
|
|
444
|
+
const candidateOrigin = credential.claims.traceOrigin;
|
|
445
|
+
const origin = candidateOrigin?.sessionId === entry.sessionId &&
|
|
446
|
+
candidateOrigin?.clientRequestId === entry.clientRequestId
|
|
447
|
+
? candidateOrigin
|
|
380
448
|
: undefined;
|
|
449
|
+
diagnostics.bind({
|
|
450
|
+
configId: credential.configId,
|
|
451
|
+
runtimeOperationId: origin?.runtimeOperationId,
|
|
452
|
+
});
|
|
453
|
+
const captured = origin ? this.capture.begin(modelCallId, credential) : undefined;
|
|
381
454
|
capturedRequest = captured?.artifact('codex_request', request.headers['content-type'] ?? 'application/json');
|
|
382
455
|
capturedResponse = captured?.artifact('codex_response');
|
|
383
456
|
if (capturedResponse)
|
|
384
457
|
captureSubmittedResponse(response, capturedResponse);
|
|
458
|
+
diagnostics.enter('request_body');
|
|
385
459
|
const requestReadMark = performance.now();
|
|
386
460
|
const chunks = [];
|
|
387
461
|
let size = 0;
|
|
388
|
-
|
|
462
|
+
await readModelRequestBody(request, abort.signal, (chunk) => {
|
|
463
|
+
diagnostics?.requestChunk(chunk.length);
|
|
389
464
|
capturedRequest?.append(chunk);
|
|
390
465
|
size += chunk.length;
|
|
391
466
|
if (size > MAX_MODEL_REQUEST_BYTES) {
|
|
392
|
-
|
|
393
|
-
return;
|
|
467
|
+
throw new ModelBrokerRequestError(413, 'model_request_too_large');
|
|
394
468
|
}
|
|
395
469
|
chunks.push(Buffer.from(chunk));
|
|
396
|
-
}
|
|
470
|
+
});
|
|
471
|
+
checkDeadline();
|
|
397
472
|
const requestReadMs = elapsed(requestReadMark);
|
|
398
473
|
capturedRequest?.finish({ sourceEnd: 'eof', protocolTerminal: 'not_applicable' });
|
|
399
474
|
requestEnded = true;
|
|
475
|
+
diagnostics.enter('prepare');
|
|
400
476
|
if (!this.valid(entry))
|
|
401
477
|
throw new Error('model_access_denied');
|
|
402
|
-
|
|
478
|
+
let body;
|
|
479
|
+
try {
|
|
480
|
+
body = JSON.parse(Buffer.concat(chunks).toString('utf8'));
|
|
481
|
+
}
|
|
482
|
+
catch {
|
|
483
|
+
checkDeadline();
|
|
484
|
+
throw new ModelBrokerRequestError(400, 'model_request_invalid_json');
|
|
485
|
+
}
|
|
486
|
+
checkDeadline();
|
|
403
487
|
if (!body || typeof body !== 'object' || Array.isArray(body)) {
|
|
404
488
|
response.writeHead(400);
|
|
405
489
|
response.end();
|
|
@@ -426,15 +510,16 @@ export class WorkspaceModelBroker {
|
|
|
426
510
|
effort,
|
|
427
511
|
};
|
|
428
512
|
}
|
|
429
|
-
|
|
513
|
+
credentialTimer = setTimeout(() => {
|
|
514
|
+
if (abort.signal.aborted)
|
|
515
|
+
return;
|
|
516
|
+
diagnostics?.abort('credential_expired');
|
|
517
|
+
abort.abort(new ModelBrokerRequestError(401, 'model_credential_expired'));
|
|
518
|
+
}, Math.max(0, Date.parse(credential.expiresAt) - Date.now()));
|
|
519
|
+
credentialTimer.unref();
|
|
430
520
|
const dispatchedAt = new Date().toISOString();
|
|
431
521
|
const dispatchedMark = performance.now();
|
|
432
522
|
const beforeDispatchMs = elapsed(receivedMark);
|
|
433
|
-
const candidateOrigin = credential.claims.traceOrigin;
|
|
434
|
-
const origin = candidateOrigin?.sessionId === entry.sessionId &&
|
|
435
|
-
candidateOrigin?.clientRequestId === entry.clientRequestId
|
|
436
|
-
? candidateOrigin
|
|
437
|
-
: undefined;
|
|
438
523
|
const broker = {
|
|
439
524
|
receivedAt,
|
|
440
525
|
dispatchedAt,
|
|
@@ -445,6 +530,9 @@ export class WorkspaceModelBroker {
|
|
|
445
530
|
let attemptId;
|
|
446
531
|
let outcome = 'error';
|
|
447
532
|
try {
|
|
533
|
+
const requestBody = JSON.stringify(body);
|
|
534
|
+
checkDeadline();
|
|
535
|
+
diagnostics.enter('upstream_headers');
|
|
448
536
|
const upstream = await fetch(platformEndpoint(this.state.config.publicBaseUrl, modelResponsesPath(credential.configId)), {
|
|
449
537
|
method: 'POST',
|
|
450
538
|
redirect: 'error',
|
|
@@ -453,6 +541,7 @@ export class WorkspaceModelBroker {
|
|
|
453
541
|
authorization: `Bearer ${credential.token}`,
|
|
454
542
|
'content-type': 'application/json',
|
|
455
543
|
'x-playcraft-model-call-id': modelCallId,
|
|
544
|
+
'x-request-id': modelCallId,
|
|
456
545
|
...(origin
|
|
457
546
|
? {
|
|
458
547
|
'x-playcraft-model-journey': JSON.stringify({
|
|
@@ -483,10 +572,11 @@ export class WorkspaceModelBroker {
|
|
|
483
572
|
}
|
|
484
573
|
: {}),
|
|
485
574
|
},
|
|
486
|
-
body:
|
|
575
|
+
body: requestBody,
|
|
487
576
|
});
|
|
488
577
|
broker.responseHeadersAt = new Date().toISOString();
|
|
489
578
|
broker.proxyResponseHeadersMs = elapsed(dispatchedMark);
|
|
579
|
+
diagnostics.headers(upstream.status, upstream.headers);
|
|
490
580
|
const reference = upstream.headers.get('x-model-attempt-id');
|
|
491
581
|
if (reference && z.string().uuid().safeParse(reference).success)
|
|
492
582
|
attemptId = reference;
|
|
@@ -498,15 +588,22 @@ export class WorkspaceModelBroker {
|
|
|
498
588
|
route.lastAdmissionReason = undefined;
|
|
499
589
|
else if (admissionReason)
|
|
500
590
|
route.lastAdmissionReason = admissionReason;
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
591
|
+
const forwardHeaders = () => {
|
|
592
|
+
if (response.headersSent)
|
|
593
|
+
return;
|
|
594
|
+
response.writeHead(upstream.status, {
|
|
595
|
+
'content-type': upstream.headers.get('content-type') ?? 'application/json',
|
|
596
|
+
'cache-control': 'no-store',
|
|
597
|
+
...(admissionReason ? { 'x-model-admission-reason': admissionReason } : {}),
|
|
598
|
+
...(retryAfter ? { 'retry-after': retryAfter } : {}),
|
|
599
|
+
});
|
|
600
|
+
};
|
|
601
|
+
const requestDiagnostics = diagnostics;
|
|
602
|
+
if (upstream.body) {
|
|
603
|
+
const observed = new Transform({
|
|
509
604
|
transform(chunk, _encoding, callback) {
|
|
605
|
+
forwardHeaders();
|
|
606
|
+
requestDiagnostics.responseChunk(chunk.length);
|
|
510
607
|
capturedResponse?.upstreamObserved(chunk.length);
|
|
511
608
|
if (chunk.length && broker.firstBodyByteAt === undefined) {
|
|
512
609
|
broker.firstBodyByteAt = new Date().toISOString();
|
|
@@ -514,9 +611,28 @@ export class WorkspaceModelBroker {
|
|
|
514
611
|
}
|
|
515
612
|
callback(null, chunk);
|
|
516
613
|
},
|
|
517
|
-
})
|
|
518
|
-
|
|
614
|
+
});
|
|
615
|
+
// Keep an uncommitted HTTP response writable so a pre-body deadline can return 504.
|
|
616
|
+
const drained = finished(response, { cleanup: true });
|
|
617
|
+
const source = Readable.fromWeb(upstream.body);
|
|
618
|
+
const transferred = pipeline(source, observed, {
|
|
619
|
+
signal: abort.signal,
|
|
620
|
+
});
|
|
621
|
+
observed.once('end', forwardHeaders);
|
|
622
|
+
observed.pipe(response);
|
|
623
|
+
try {
|
|
624
|
+
await Promise.all([transferred, drained]);
|
|
625
|
+
}
|
|
626
|
+
catch (error) {
|
|
627
|
+
source.destroy();
|
|
628
|
+
observed.destroy();
|
|
629
|
+
throw error;
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
else {
|
|
633
|
+
forwardHeaders();
|
|
519
634
|
response.end();
|
|
635
|
+
}
|
|
520
636
|
outcome = 'completed';
|
|
521
637
|
}
|
|
522
638
|
catch (error) {
|
|
@@ -524,7 +640,7 @@ export class WorkspaceModelBroker {
|
|
|
524
640
|
throw error;
|
|
525
641
|
}
|
|
526
642
|
finally {
|
|
527
|
-
clearTimeout(
|
|
643
|
+
clearTimeout(credentialTimer);
|
|
528
644
|
broker.completedAt = new Date().toISOString();
|
|
529
645
|
broker.proxyResponseDurationMs = elapsed(dispatchedMark);
|
|
530
646
|
if (origin && attemptId) {
|
|
@@ -541,20 +657,41 @@ export class WorkspaceModelBroker {
|
|
|
541
657
|
}
|
|
542
658
|
}
|
|
543
659
|
}
|
|
544
|
-
catch {
|
|
545
|
-
if (
|
|
546
|
-
|
|
660
|
+
catch (error) {
|
|
661
|
+
if (deadlineTimer &&
|
|
662
|
+
!abort.signal.aborted &&
|
|
663
|
+
performance.now() - receivedMark >= this.requestBudgetMs) {
|
|
664
|
+
diagnostics?.abort('deadline_exceeded');
|
|
665
|
+
abort.abort(new ModelBrokerRequestError(504, 'model_request_deadline_exceeded'));
|
|
666
|
+
}
|
|
667
|
+
failure = error;
|
|
668
|
+
const rejected = modelBrokerFailure(error, abort.signal);
|
|
669
|
+
if (!response.headersSent && !response.destroyed) {
|
|
670
|
+
response.writeHead(rejected.status, {
|
|
547
671
|
'content-type': 'application/json',
|
|
548
672
|
'cache-control': 'no-store',
|
|
673
|
+
...(!request.complete ? { connection: 'close' } : {}),
|
|
549
674
|
});
|
|
550
675
|
response.end(JSON.stringify({
|
|
551
|
-
error: {
|
|
676
|
+
error: {
|
|
677
|
+
message: rejected.code,
|
|
678
|
+
type: rejected.status === 499
|
|
679
|
+
? 'request_cancelled'
|
|
680
|
+
: rejected.status < 500
|
|
681
|
+
? 'invalid_request_error'
|
|
682
|
+
: 'server_error',
|
|
683
|
+
code: rejected.code,
|
|
684
|
+
...(rejected.status === 413 ? { maxBytes: MAX_MODEL_REQUEST_BYTES } : {}),
|
|
685
|
+
},
|
|
552
686
|
}));
|
|
553
687
|
}
|
|
554
688
|
else
|
|
555
689
|
response.destroy();
|
|
556
690
|
}
|
|
557
691
|
finally {
|
|
692
|
+
clearTimeout(deadlineTimer);
|
|
693
|
+
clearTimeout(credentialTimer);
|
|
694
|
+
diagnostics?.finish(response.statusCode, response.writableFinished, abort.signal.aborted, failure);
|
|
558
695
|
if (!requestEnded)
|
|
559
696
|
capturedRequest?.finish({
|
|
560
697
|
sourceEnd: abort.signal.aborted ? 'aborted' : 'io_error',
|
|
@@ -237,6 +237,7 @@ export class CaptureCall {
|
|
|
237
237
|
token;
|
|
238
238
|
tokenExpires = 0;
|
|
239
239
|
uploading = false;
|
|
240
|
+
pendingUpload = false;
|
|
240
241
|
stopped = false;
|
|
241
242
|
reservedBytes = 0;
|
|
242
243
|
removing;
|
|
@@ -297,7 +298,7 @@ export class CaptureCall {
|
|
|
297
298
|
record.parts = partNo + 1;
|
|
298
299
|
await this.persist();
|
|
299
300
|
}
|
|
300
|
-
catch
|
|
301
|
+
catch {
|
|
301
302
|
if (!written)
|
|
302
303
|
this.release(bytes.length);
|
|
303
304
|
this.failedStages.add(record.stage);
|
|
@@ -400,7 +401,11 @@ export class CaptureCall {
|
|
|
400
401
|
});
|
|
401
402
|
if (!response.ok) {
|
|
402
403
|
await response.body?.cancel();
|
|
403
|
-
if ([401, 403, 404, 410].includes(response.status)
|
|
404
|
+
if ([401, 403, 404, 410].includes(response.status) ||
|
|
405
|
+
(response.status >= 400 &&
|
|
406
|
+
response.status < 500 &&
|
|
407
|
+
response.status !== 408 &&
|
|
408
|
+
response.status !== 429))
|
|
404
409
|
await this.remove();
|
|
405
410
|
throw new Error('capture_authority_unavailable');
|
|
406
411
|
}
|
|
@@ -422,6 +427,12 @@ export class CaptureCall {
|
|
|
422
427
|
});
|
|
423
428
|
if (!response.ok) {
|
|
424
429
|
const terminal = response.status === 410 ||
|
|
430
|
+
(response.status >= 400 &&
|
|
431
|
+
response.status < 500 &&
|
|
432
|
+
response.status !== 401 &&
|
|
433
|
+
response.status !== 403 &&
|
|
434
|
+
response.status !== 408 &&
|
|
435
|
+
response.status !== 429) ||
|
|
425
436
|
((response.status === 401 || response.status === 403) && this.tokenExpires > 0);
|
|
426
437
|
if (response.status === 401 || response.status === 403) {
|
|
427
438
|
this.token = undefined;
|
|
@@ -445,9 +456,15 @@ export class CaptureCall {
|
|
|
445
456
|
}
|
|
446
457
|
}
|
|
447
458
|
async upload() {
|
|
448
|
-
if (this.
|
|
459
|
+
if (this.stopped)
|
|
460
|
+
return;
|
|
461
|
+
if (this.uploading) {
|
|
462
|
+
this.pendingUpload = true;
|
|
449
463
|
return;
|
|
464
|
+
}
|
|
450
465
|
this.uploading = true;
|
|
466
|
+
this.pendingUpload = false;
|
|
467
|
+
let shouldRetry = false;
|
|
451
468
|
try {
|
|
452
469
|
await this.writes;
|
|
453
470
|
if (this.stopped)
|
|
@@ -456,6 +473,7 @@ export class CaptureCall {
|
|
|
456
473
|
await this.remove();
|
|
457
474
|
return;
|
|
458
475
|
}
|
|
476
|
+
let completedStages = 0;
|
|
459
477
|
await uploadSlot(async () => {
|
|
460
478
|
if (!this.manifest.identity.captureCallRowId) {
|
|
461
479
|
const response = await this.send('/register', {
|
|
@@ -476,12 +494,18 @@ export class CaptureCall {
|
|
|
476
494
|
if (status.call?.callRowId !== this.manifest.identity.captureCallRowId ||
|
|
477
495
|
status.call.modelCallId !== this.manifest.identity.modelCallId ||
|
|
478
496
|
!Array.isArray(status.artifacts)) {
|
|
497
|
+
await this.remove();
|
|
479
498
|
throw new Error('capture_status_identity_mismatch');
|
|
480
499
|
}
|
|
481
500
|
for (const record of this.manifest.artifacts) {
|
|
482
501
|
if (!record.end)
|
|
483
502
|
continue;
|
|
484
503
|
const remote = status.artifacts.find((item) => item.stage === record.stage);
|
|
504
|
+
if (remote?.archiveState &&
|
|
505
|
+
['failed', 'expired', 'deleted'].includes(remote.archiveState)) {
|
|
506
|
+
completedStages++;
|
|
507
|
+
continue;
|
|
508
|
+
}
|
|
485
509
|
const confirmed = new Map((remote?.confirmedParts ?? []).map((part) => [part.partNo, part]));
|
|
486
510
|
const hash = createHash('sha256');
|
|
487
511
|
for (let partNo = 0; partNo < record.parts; partNo++) {
|
|
@@ -490,8 +514,10 @@ export class CaptureCall {
|
|
|
490
514
|
const sha256 = createHash('sha256').update(bytes).digest('hex');
|
|
491
515
|
const part = confirmed.get(partNo);
|
|
492
516
|
if (part) {
|
|
493
|
-
if (part.sha256 !== sha256 || part.rawBytes !== bytes.length)
|
|
517
|
+
if (part.sha256 !== sha256 || part.rawBytes !== bytes.length) {
|
|
518
|
+
await this.remove();
|
|
494
519
|
throw new Error('capture_part_confirmation_mismatch');
|
|
520
|
+
}
|
|
495
521
|
continue;
|
|
496
522
|
}
|
|
497
523
|
const response = await this.send(`/artifacts/${record.stage}/chunks/${partNo}`, {
|
|
@@ -508,7 +534,14 @@ export class CaptureCall {
|
|
|
508
534
|
}
|
|
509
535
|
const sha256 = hash.digest('hex');
|
|
510
536
|
if (remote?.archiveState === 'archived') {
|
|
511
|
-
|
|
537
|
+
try {
|
|
538
|
+
this.verifyArchive(record, sha256, remote);
|
|
539
|
+
}
|
|
540
|
+
catch (error) {
|
|
541
|
+
await this.remove();
|
|
542
|
+
throw error;
|
|
543
|
+
}
|
|
544
|
+
completedStages++;
|
|
512
545
|
continue;
|
|
513
546
|
}
|
|
514
547
|
const response = await this.send(`/artifacts/${record.stage}/finalize`, {
|
|
@@ -521,10 +554,20 @@ export class CaptureCall {
|
|
|
521
554
|
sha256,
|
|
522
555
|
}),
|
|
523
556
|
});
|
|
524
|
-
|
|
557
|
+
const finalized = (await response.json());
|
|
558
|
+
try {
|
|
559
|
+
this.verifyArchive(record, sha256, finalized);
|
|
560
|
+
}
|
|
561
|
+
catch (error) {
|
|
562
|
+
await this.remove();
|
|
563
|
+
throw error;
|
|
564
|
+
}
|
|
565
|
+
completedStages++;
|
|
525
566
|
}
|
|
526
567
|
});
|
|
527
|
-
if (
|
|
568
|
+
if (!this.pendingUpload &&
|
|
569
|
+
(this.manifest.artifacts.length === 2 || this.artifacts.size === 0) &&
|
|
570
|
+
completedStages === this.manifest.artifacts.length &&
|
|
528
571
|
this.manifest.artifacts.every((item) => item.end)) {
|
|
529
572
|
await this.remove();
|
|
530
573
|
return;
|
|
@@ -532,14 +575,20 @@ export class CaptureCall {
|
|
|
532
575
|
}
|
|
533
576
|
catch {
|
|
534
577
|
/* Retry only archival transport, never model execution. */
|
|
578
|
+
shouldRetry = !this.stopped;
|
|
535
579
|
}
|
|
536
580
|
finally {
|
|
537
581
|
this.uploading = false;
|
|
538
582
|
if (!this.stopped) {
|
|
539
|
-
this.
|
|
583
|
+
if (this.pendingUpload && !shouldRetry) {
|
|
540
584
|
void this.upload();
|
|
541
|
-
}
|
|
542
|
-
|
|
585
|
+
}
|
|
586
|
+
else if (shouldRetry) {
|
|
587
|
+
this.retry = setTimeout(() => {
|
|
588
|
+
void this.upload();
|
|
589
|
+
}, 5_000);
|
|
590
|
+
this.retry.unref();
|
|
591
|
+
}
|
|
543
592
|
}
|
|
544
593
|
}
|
|
545
594
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@playcraft/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.87",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -58,8 +58,8 @@
|
|
|
58
58
|
"@gltf-transform/core": "^4.3.0",
|
|
59
59
|
"@gltf-transform/extensions": "^4.3.0",
|
|
60
60
|
"@gltf-transform/functions": "^4.3.0",
|
|
61
|
-
"@playcraft/build": "0.1.
|
|
62
|
-
"@playcraft/common": "0.1.
|
|
61
|
+
"@playcraft/build": "0.1.87",
|
|
62
|
+
"@playcraft/common": "0.1.87",
|
|
63
63
|
"better-sqlite3-pinned": "npm:better-sqlite3@13.0.3",
|
|
64
64
|
"chokidar": "^4.0.3",
|
|
65
65
|
"commander": "^13.1.0",
|