@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.
@@ -1,41 +1,23 @@
1
- import { createHash, randomBytes, randomUUID, timingSafeEqual } from 'node:crypto';
1
+ import { createHash, randomBytes, timingSafeEqual } from 'node:crypto';
2
2
  import { createServer } from 'node:http';
3
3
  import { Readable, Transform } from 'node:stream';
4
4
  import { 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';
8
- import { PLATFORM_ROUTES, modelResponsesPath, platformEndpoint, workspaceExecutionTraceOriginSchema, } from '@playcraft/common/cloud-workspace/runtime';
8
+ import { PLATFORM_ROUTES, modelResponsesPath, platformEndpoint, workspaceExecutionTraceOriginSchema } from '@playcraft/common/cloud-workspace/runtime';
9
9
  import { WorkspaceGenerationProvenanceBroker } from './generation-provenance-broker.js';
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 { WorkspaceModelCapture, captureSubmittedResponse, } from './model-capture.js';
14
13
  const credentialSchema = z.object({
15
- token: z.string().min(1),
16
- expiresAt: z.string().datetime(),
17
- configId: z.string().uuid(),
18
- model: z.string().min(1).max(128),
14
+ token: z.string().min(1), expiresAt: z.string().datetime(),
15
+ configId: z.string().uuid(), model: z.string().min(1).max(128),
19
16
  supportsReasoning: z.boolean().optional(),
20
17
  claims: z.object({
21
- environment: z.string(),
22
- userId: z.string(),
23
- workspaceId: z.string(),
24
- runtimeId: z.string(),
25
- generation: z.number().int(),
26
- runtimeEpoch: z.number().int(),
27
- traceOrigin: z
28
- .custom((value) => workspaceExecutionTraceOriginSchema.safeParse(value).success)
29
- .optional(),
30
- capturePolicy: z
31
- .object({
32
- enabled: z.literal(true),
33
- policyVersion: z.literal('workspace-model-capture-v1'),
34
- maxRequestBytes: z.number().int().positive(),
35
- maxResponseBytes: z.number().int().positive(),
36
- expiresAt: z.string().datetime(),
37
- })
38
- .optional(),
18
+ environment: z.string(), userId: z.string(), workspaceId: z.string(), runtimeId: z.string(),
19
+ generation: z.number().int(), runtimeEpoch: z.number().int(),
20
+ traceOrigin: z.custom(value => workspaceExecutionTraceOriginSchema.safeParse(value).success).optional(),
39
21
  }),
40
22
  });
41
23
  const MAX_MODEL_REQUEST_BYTES = 128 * 1024 * 1024;
@@ -53,14 +35,12 @@ export class WorkspaceModelBroker {
53
35
  references;
54
36
  executionContext;
55
37
  skills;
56
- capture;
57
38
  constructor(state) {
58
39
  this.state = state;
59
40
  this.generation = new WorkspaceGenerationProvenanceBroker(state);
60
41
  this.references = new WorkspaceRemixReferencesBroker(state);
61
42
  this.executionContext = new WorkspaceExecutionContextBroker(state);
62
43
  this.skills = new WorkspaceSkillDiscoveryBroker(state);
63
- this.capture = new WorkspaceModelCapture(state);
64
44
  }
65
45
  async start() {
66
46
  if (this.server)
@@ -77,7 +57,6 @@ export class WorkspaceModelBroker {
77
57
  if (!address || typeof address === 'string')
78
58
  throw new Error('model_broker_unavailable');
79
59
  this.baseUrl = `http://127.0.0.1:${address.port}`;
80
- await this.capture.start();
81
60
  }
82
61
  async threadConfig(cwd, modelConfigId, sessionId, reasoningEffort) {
83
62
  const { activate, ...settings } = await this.prepareThreadConfig(cwd, modelConfigId, sessionId, reasoningEffort);
@@ -90,44 +69,24 @@ export class WorkspaceModelBroker {
90
69
  const { workspaceId } = await workspaceContextFromRoot(this.state, cwd);
91
70
  if (modelConfigId !== undefined && !z.string().uuid().safeParse(modelConfigId).success)
92
71
  throw new Error('model_config_denied');
93
- const routeKey = sessionId
94
- ? `${workspaceId}/${createHash('sha256').update(sessionId).digest('hex').slice(0, 32)}`
95
- : workspaceId;
72
+ const routeKey = sessionId ? `${workspaceId}/${createHash('sha256').update(sessionId).digest('hex').slice(0, 32)}` : workspaceId;
96
73
  let route = this.routes.get(routeKey);
97
- const selected = modelConfigId ??
98
- (route?.epoch === this.state.runtimeEpoch ? route.selected?.entry.modelConfigId : undefined);
74
+ const selected = modelConfigId ?? (route?.epoch === this.state.runtimeEpoch ? route.selected?.entry.modelConfigId : undefined);
99
75
  const cacheKey = `${workspaceId}:${selected ?? 'default'}:${reasoningEffort ?? 'default'}:${sessionId ?? ''}:${clientRequestId ?? ''}`;
100
- let entry = this.entries.get(cacheKey) ??
101
- [...this.entries.values()].find((item) => item.workspaceId === workspaceId &&
102
- item.epoch === this.state.runtimeEpoch &&
103
- item.modelConfigId === selected &&
104
- item.sessionId === sessionId &&
105
- item.clientRequestId === clientRequestId &&
106
- item.reasoningEffort === reasoningEffort);
76
+ let entry = this.entries.get(cacheKey) ?? [...this.entries.values()].find(item => item.workspaceId === workspaceId && item.epoch === this.state.runtimeEpoch && item.modelConfigId === selected
77
+ && item.sessionId === sessionId && item.clientRequestId === clientRequestId
78
+ && item.reasoningEffort === reasoningEffort);
107
79
  if (!entry || entry.epoch !== this.state.runtimeEpoch) {
108
- entry = {
109
- workspaceId,
110
- epoch: this.state.runtimeEpoch,
111
- cacheKey,
112
- modelConfigId: selected,
113
- reasoningEffort,
114
- sessionId,
115
- clientRequestId,
116
- };
80
+ entry = { workspaceId, epoch: this.state.runtimeEpoch, cacheKey, modelConfigId: selected, reasoningEffort, sessionId, clientRequestId };
117
81
  this.entries.set(cacheKey, entry);
118
82
  }
119
83
  // Resolve current model metadata and permission at every native turn boundary.
120
84
  const credential = await this.credential(entry, true);
121
85
  const boundaryId = sessionId && clientRequestId ? await this.generation.prepare() : undefined;
122
86
  if (!route || route.epoch !== this.state.runtimeEpoch) {
123
- route = {
124
- workspaceId,
125
- epoch: this.state.runtimeEpoch,
126
- capability: randomBytes(32).toString('base64url'),
127
- generationCapability: randomBytes(32).toString('base64url'),
128
- referencesCapability: randomBytes(32).toString('base64url'),
129
- skillsCapability: randomBytes(32).toString('base64url'),
130
- };
87
+ route = { workspaceId, epoch: this.state.runtimeEpoch, capability: randomBytes(32).toString('base64url'),
88
+ generationCapability: randomBytes(32).toString('base64url'), referencesCapability: randomBytes(32).toString('base64url'),
89
+ skillsCapability: randomBytes(32).toString('base64url') };
131
90
  this.routes.set(routeKey, route);
132
91
  }
133
92
  // Each session pins its prepared model; shared credential renewal cannot change an active turn.
@@ -136,67 +95,41 @@ export class WorkspaceModelBroker {
136
95
  if (!this.valid(entry) || this.routes.get(routeKey) !== route)
137
96
  throw new Error('model_access_denied');
138
97
  route.lastAdmissionReason = undefined;
139
- route.selected = {
140
- entry,
141
- model: credential.model,
142
- ...(sessionId && clientRequestId
143
- ? {
144
- feedback: {
145
- sessionId,
146
- clientRequestId,
147
- cwd,
148
- boundaryId,
149
- readOnly,
150
- ...(originCwd ? { originCwd } : {}),
151
- },
152
- }
153
- : {}),
154
- };
98
+ route.selected = { entry, model: credential.model,
99
+ ...(sessionId && clientRequestId ? { feedback: { sessionId, clientRequestId, cwd, boundaryId, readOnly,
100
+ ...(originCwd ? { originCwd } : {}) } } : {}) };
155
101
  },
156
102
  model: credential.model,
157
103
  modelConfigId: credential.configId,
158
104
  modelProvider: 'playcraft_workspace',
159
105
  config: {
160
- ...(sessionId
161
- ? {
162
- 'shell_environment_policy.set': {
163
- PLAYCRAFT_AGENT_GENERATION_ENDPOINT: `${this.baseUrl}/${routeKey}/generation-provenance`,
164
- PLAYCRAFT_AGENT_GENERATION_CAPABILITY: route.generationCapability,
165
- PLAYCRAFT_REMIX_REFERENCES_ENDPOINT: `${this.baseUrl}/${routeKey}/remix-references`,
166
- PLAYCRAFT_REMIX_REFERENCES_CAPABILITY: route.referencesCapability,
167
- PLAYCRAFT_SKILLS_ENDPOINT: `${this.baseUrl}/${routeKey}/skill-discovery`,
168
- PLAYCRAFT_SKILLS_CAPABILITY: route.skillsCapability,
169
- },
170
- }
171
- : {}),
106
+ ...(sessionId ? { 'shell_environment_policy.set': {
107
+ PLAYCRAFT_AGENT_GENERATION_ENDPOINT: `${this.baseUrl}/${routeKey}/generation-provenance`,
108
+ PLAYCRAFT_AGENT_GENERATION_CAPABILITY: route.generationCapability,
109
+ PLAYCRAFT_REMIX_REFERENCES_ENDPOINT: `${this.baseUrl}/${routeKey}/remix-references`,
110
+ PLAYCRAFT_REMIX_REFERENCES_CAPABILITY: route.referencesCapability,
111
+ PLAYCRAFT_SKILLS_ENDPOINT: `${this.baseUrl}/${routeKey}/skill-discovery`,
112
+ PLAYCRAFT_SKILLS_CAPABILITY: route.skillsCapability,
113
+ } } : {}),
172
114
  'model_providers.playcraft_workspace': {
173
- name: 'PlayCraft Workspace',
174
- base_url: `${this.baseUrl}/${routeKey}`,
175
- wire_api: 'responses',
176
- requires_openai_auth: false,
115
+ name: 'PlayCraft Workspace', base_url: `${this.baseUrl}/${routeKey}`,
116
+ wire_api: 'responses', requires_openai_auth: false,
177
117
  http_headers: { Authorization: `Bearer ${route.capability}` },
178
118
  },
179
119
  },
180
120
  };
181
121
  }
182
122
  valid(entry) {
183
- return Boolean(this.server &&
184
- !this.state.journal.destroyed() &&
185
- this.state.runtimeEpoch === entry.epoch &&
186
- this.state.journal.getWorkspace(entry.workspaceId) &&
187
- this.entries.get(entry.cacheKey) === entry);
123
+ return Boolean(this.server && !this.state.journal.destroyed()
124
+ && this.state.runtimeEpoch === entry.epoch && this.state.journal.getWorkspace(entry.workspaceId)
125
+ && this.entries.get(entry.cacheKey) === entry);
188
126
  }
189
127
  /** Called immediately before the native turn/start write, within this broker's process clock. */
190
128
  markCodexDispatched(cwd, sessionId, clientRequestId) {
191
- const workspaceId = /^(basic|remix-)/.test(basename(cwd))
192
- ? basename(dirname(cwd))
193
- : basename(cwd);
129
+ const workspaceId = /^(basic|remix-)/.test(basename(cwd)) ? basename(dirname(cwd)) : basename(cwd);
194
130
  const route = this.routes.get(`${workspaceId}/${createHash('sha256').update(sessionId).digest('hex').slice(0, 32)}`);
195
131
  const entry = route?.selected?.entry;
196
- if (!entry ||
197
- !this.valid(entry) ||
198
- entry.sessionId !== sessionId ||
199
- entry.clientRequestId !== clientRequestId)
132
+ if (!entry || !this.valid(entry) || entry.sessionId !== sessionId || entry.clientRequestId !== clientRequestId)
200
133
  return;
201
134
  entry.codexDispatchedAt = new Date().toISOString();
202
135
  entry.codexDispatchMark = performance.now();
@@ -204,9 +137,7 @@ export class WorkspaceModelBroker {
204
137
  async credential(entry, refresh = false) {
205
138
  if (!this.valid(entry))
206
139
  throw new Error('model_access_denied');
207
- if (!refresh &&
208
- entry.credential &&
209
- Date.parse(entry.credential.expiresAt) > Date.now() + 60_000)
140
+ if (!refresh && entry.credential && Date.parse(entry.credential.expiresAt) > Date.now() + 60_000)
210
141
  return entry.credential;
211
142
  let pending = this.pending.get(entry);
212
143
  if (!pending) {
@@ -219,48 +150,26 @@ export class WorkspaceModelBroker {
219
150
  const config = this.state.config;
220
151
  const endpoint = platformEndpoint(config.publicBaseUrl, PLATFORM_ROUTES.modelCredentials);
221
152
  const response = await fetch(endpoint, {
222
- method: 'POST',
223
- redirect: 'error',
224
- signal: AbortSignal.timeout(10_000),
225
- headers: {
226
- authorization: `Bearer ${config.controlToken}`,
227
- 'content-type': 'application/json',
228
- 'playcraft-env': config.environment,
229
- ...(this.capture.supported ? { 'x-playcraft-model-capture-protocol': '1' } : {}),
230
- },
231
- body: JSON.stringify({
232
- runtimeId: config.runtimeId,
233
- generation: config.generation,
234
- runtimeEpoch: entry.epoch,
235
- workspaceId: entry.workspaceId,
153
+ method: 'POST', redirect: 'error', signal: AbortSignal.timeout(10_000),
154
+ headers: { authorization: `Bearer ${config.controlToken}`, 'content-type': 'application/json', 'playcraft-env': config.environment },
155
+ body: JSON.stringify({ runtimeId: config.runtimeId, generation: config.generation,
156
+ runtimeEpoch: entry.epoch, workspaceId: entry.workspaceId,
236
157
  ...(entry.sessionId ? { sessionId: entry.sessionId } : {}),
237
158
  ...(entry.clientRequestId ? { clientRequestId: entry.clientRequestId } : {}),
238
159
  ...(entry.modelConfigId ? { modelConfigId: entry.modelConfigId } : {}),
239
- ...(entry.reasoningEffort ? { reasoningEffort: entry.reasoningEffort } : {}),
240
- }),
160
+ ...(entry.reasoningEffort ? { reasoningEffort: entry.reasoningEffort } : {}) }),
241
161
  });
242
162
  if (!response.ok) {
243
- const error = (await response.json().catch(() => ({})));
244
- throw new Error([
245
- 'model_config_required',
246
- 'model_config_denied',
247
- 'reasoning_effort_denied',
248
- 'reasoning_effort_unsupported',
249
- ].includes(error.reason ?? '')
250
- ? error.reason
251
- : 'model_credential_unavailable');
163
+ const error = await response.json().catch(() => ({}));
164
+ throw new Error(['model_config_required', 'model_config_denied', 'reasoning_effort_denied', 'reasoning_effort_unsupported'].includes(error.reason ?? '') ? error.reason : 'model_credential_unavailable');
252
165
  }
253
166
  const credential = credentialSchema.parse(await response.json());
254
167
  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)
168
+ if (!this.valid(entry) || claims.workspaceId !== entry.workspaceId || claims.runtimeId !== config.runtimeId
169
+ || claims.userId !== config.userId || claims.environment !== config.environment
170
+ || claims.generation !== config.generation || claims.runtimeEpoch !== entry.epoch
171
+ || (entry.modelConfigId !== undefined && credential.configId !== entry.modelConfigId)
172
+ || Date.parse(credential.expiresAt) <= Date.now() + 1000)
264
173
  throw new Error('model_credential_scope_mismatch');
265
174
  entry.modelConfigId = credential.configId;
266
175
  entry.credential = credential;
@@ -273,9 +182,6 @@ export class WorkspaceModelBroker {
273
182
  const abort = new AbortController();
274
183
  this.requests.add(abort);
275
184
  let activeRoute;
276
- let capturedRequest;
277
- let capturedResponse;
278
- let requestEnded = false;
279
185
  const cancel = () => abort.abort();
280
186
  response.once('close', cancel);
281
187
  try {
@@ -288,21 +194,10 @@ export class WorkspaceModelBroker {
288
194
  const contextRequest = match?.[2] === 'execution-context';
289
195
  const referencesRequest = match?.[2] === 'remix-references';
290
196
  const skillsRequest = match?.[2]?.startsWith('skill-discovery/');
291
- const expected = Buffer.from(route
292
- ? `Bearer ${generationRequest || contextRequest
293
- ? route.generationCapability
294
- : referencesRequest
295
- ? route.referencesCapability
296
- : skillsRequest
297
- ? route.skillsCapability
298
- : route.capability}`
299
- : '');
300
- if (request.method !== 'POST' ||
301
- !entry ||
302
- !expected.length ||
303
- presented.length !== expected.length ||
304
- !timingSafeEqual(presented, expected) ||
305
- !this.valid(entry)) {
197
+ const expected = Buffer.from(route ? `Bearer ${generationRequest || contextRequest ? route.generationCapability
198
+ : referencesRequest ? route.referencesCapability : skillsRequest ? route.skillsCapability : route.capability}` : '');
199
+ if (request.method !== 'POST' || !entry || !expected.length || presented.length !== expected.length
200
+ || !timingSafeEqual(presented, expected) || !this.valid(entry)) {
306
201
  response.writeHead(403);
307
202
  response.end();
308
203
  return;
@@ -345,23 +240,12 @@ export class WorkspaceModelBroker {
345
240
  return;
346
241
  }
347
242
  const rejectOversizedBody = () => {
348
- response.writeHead(413, {
349
- 'content-type': 'application/json',
350
- 'cache-control': 'no-store',
351
- });
352
- response.end(JSON.stringify({
353
- error: {
354
- message: 'model_request_too_large',
355
- type: 'invalid_request_error',
356
- code: 'model_request_too_large',
357
- maxBytes: MAX_MODEL_REQUEST_BYTES,
358
- },
359
- }));
243
+ response.writeHead(413, { 'content-type': 'application/json', 'cache-control': 'no-store' });
244
+ response.end(JSON.stringify({ error: { message: 'model_request_too_large',
245
+ type: 'invalid_request_error', code: 'model_request_too_large', maxBytes: MAX_MODEL_REQUEST_BYTES } }));
360
246
  };
361
247
  const declaredLength = request.headers['content-length'];
362
- if (declaredLength !== undefined &&
363
- /^\d+$/.test(declaredLength) &&
364
- Number(declaredLength) > MAX_MODEL_REQUEST_BYTES) {
248
+ if (declaredLength !== undefined && /^\d+$/.test(declaredLength) && Number(declaredLength) > MAX_MODEL_REQUEST_BYTES) {
365
249
  rejectOversizedBody();
366
250
  return;
367
251
  }
@@ -372,21 +256,10 @@ export class WorkspaceModelBroker {
372
256
  const credentialMark = performance.now();
373
257
  const credential = await this.credential(entry);
374
258
  const credentialWaitMs = elapsed(credentialMark);
375
- const modelCallId = randomUUID();
376
- const candidateCaptureOrigin = credential.claims.traceOrigin;
377
- const captured = candidateCaptureOrigin?.sessionId === entry.sessionId &&
378
- candidateCaptureOrigin?.clientRequestId === entry.clientRequestId
379
- ? this.capture.begin(modelCallId, credential)
380
- : undefined;
381
- capturedRequest = captured?.artifact('codex_request', request.headers['content-type'] ?? 'application/json');
382
- capturedResponse = captured?.artifact('codex_response');
383
- if (capturedResponse)
384
- captureSubmittedResponse(response, capturedResponse);
385
259
  const requestReadMark = performance.now();
386
260
  const chunks = [];
387
261
  let size = 0;
388
262
  for await (const chunk of request) {
389
- capturedRequest?.append(chunk);
390
263
  size += chunk.length;
391
264
  if (size > MAX_MODEL_REQUEST_BYTES) {
392
265
  rejectOversizedBody();
@@ -395,8 +268,6 @@ export class WorkspaceModelBroker {
395
268
  chunks.push(Buffer.from(chunk));
396
269
  }
397
270
  const requestReadMs = elapsed(requestReadMark);
398
- capturedRequest?.finish({ sourceEnd: 'eof', protocolTerminal: 'not_applicable' });
399
- requestEnded = true;
400
271
  if (!this.valid(entry))
401
272
  throw new Error('model_access_denied');
402
273
  const body = JSON.parse(Buffer.concat(chunks).toString('utf8'));
@@ -417,12 +288,9 @@ export class WorkspaceModelBroker {
417
288
  const reasoning = body.reasoning;
418
289
  const modelName = selected.model.split('/').pop() ?? selected.model;
419
290
  const effort = entry.reasoningEffort === 'max' && /^o\d|gpt-[5-9]/i.test(modelName)
420
- ? 'xhigh'
421
- : entry.reasoningEffort;
291
+ ? 'xhigh' : entry.reasoningEffort;
422
292
  body.reasoning = {
423
- ...(reasoning && typeof reasoning === 'object' && !Array.isArray(reasoning)
424
- ? reasoning
425
- : {}),
293
+ ...(reasoning && typeof reasoning === 'object' && !Array.isArray(reasoning) ? reasoning : {}),
426
294
  effort,
427
295
  };
428
296
  }
@@ -431,58 +299,26 @@ export class WorkspaceModelBroker {
431
299
  const dispatchedMark = performance.now();
432
300
  const beforeDispatchMs = elapsed(receivedMark);
433
301
  const candidateOrigin = credential.claims.traceOrigin;
434
- const origin = candidateOrigin?.sessionId === entry.sessionId &&
435
- candidateOrigin?.clientRequestId === entry.clientRequestId
436
- ? candidateOrigin
437
- : undefined;
438
- const broker = {
439
- receivedAt,
440
- dispatchedAt,
441
- credentialWaitMs,
442
- requestReadMs,
443
- beforeDispatchMs,
444
- };
302
+ const origin = candidateOrigin?.sessionId === entry.sessionId && candidateOrigin?.clientRequestId === entry.clientRequestId
303
+ ? candidateOrigin : undefined;
304
+ const broker = { receivedAt, dispatchedAt,
305
+ credentialWaitMs, requestReadMs, beforeDispatchMs };
445
306
  let attemptId;
446
307
  let outcome = 'error';
447
308
  try {
448
309
  const upstream = await fetch(platformEndpoint(this.state.config.publicBaseUrl, modelResponsesPath(credential.configId)), {
449
- method: 'POST',
450
- redirect: 'error',
451
- signal: abort.signal,
452
- headers: {
453
- authorization: `Bearer ${credential.token}`,
454
- 'content-type': 'application/json',
455
- 'x-playcraft-model-call-id': modelCallId,
456
- ...(origin
457
- ? {
458
- 'x-playcraft-model-journey': JSON.stringify({
459
- version: 1,
460
- runtimeOperationId: origin.runtimeOperationId,
461
- sessionId: origin.sessionId,
462
- clientRequestId: origin.clientRequestId,
463
- ...(entry.codexDispatchedAt
464
- ? { codexDispatchedAt: entry.codexDispatchedAt }
465
- : {}),
466
- brokerReceivedAt: receivedAt,
467
- brokerDispatchedAt: dispatchedAt,
468
- credentialWaitMs,
469
- requestReadMs,
470
- beforeDispatchMs,
471
- ...(entry.codexDispatchMark === undefined
472
- ? {}
473
- : {
474
- codexDispatchToBrokerMs: Math.max(0, Math.round(receivedMark - entry.codexDispatchMark)),
475
- }),
476
- }),
477
- }
478
- : {}),
479
- ...(selected?.feedback
480
- ? {
481
- 'x-playcraft-cloud-session-id': selected.feedback.sessionId,
482
- 'x-playcraft-client-request-id': selected.feedback.clientRequestId,
483
- }
484
- : {}),
485
- },
310
+ method: 'POST', redirect: 'error', signal: abort.signal,
311
+ headers: { authorization: `Bearer ${credential.token}`, 'content-type': 'application/json',
312
+ ...(origin ? { 'x-playcraft-model-journey': JSON.stringify({ version: 1,
313
+ runtimeOperationId: origin.runtimeOperationId, sessionId: origin.sessionId, clientRequestId: origin.clientRequestId,
314
+ ...(entry.codexDispatchedAt ? { codexDispatchedAt: entry.codexDispatchedAt } : {}),
315
+ brokerReceivedAt: receivedAt, brokerDispatchedAt: dispatchedAt, credentialWaitMs, requestReadMs, beforeDispatchMs,
316
+ ...(entry.codexDispatchMark === undefined ? {} : { codexDispatchToBrokerMs: Math.max(0, Math.round(receivedMark - entry.codexDispatchMark)) }),
317
+ }) } : {}),
318
+ ...(selected?.feedback ? {
319
+ 'x-playcraft-cloud-session-id': selected.feedback.sessionId,
320
+ 'x-playcraft-client-request-id': selected.feedback.clientRequestId,
321
+ } : {}) },
486
322
  body: JSON.stringify(body),
487
323
  });
488
324
  broker.responseHeadersAt = new Date().toISOString();
@@ -507,7 +343,6 @@ export class WorkspaceModelBroker {
507
343
  if (upstream.body)
508
344
  await pipeline(Readable.fromWeb(upstream.body), new Transform({
509
345
  transform(chunk, _encoding, callback) {
510
- capturedResponse?.upstreamObserved(chunk.length);
511
346
  if (chunk.length && broker.firstBodyByteAt === undefined) {
512
347
  broker.firstBodyByteAt = new Date().toISOString();
513
348
  broker.proxyFirstBodyByteMs = elapsed(dispatchedMark);
@@ -529,37 +364,21 @@ export class WorkspaceModelBroker {
529
364
  broker.proxyResponseDurationMs = elapsed(dispatchedMark);
530
365
  if (origin && attemptId) {
531
366
  // Telemetry is sent after transfer and cannot delay or change the Codex model response.
532
- void this.reportObservation(credential.token, {
533
- version: 1,
534
- attemptId,
535
- runtimeOperationId: origin.runtimeOperationId,
536
- sessionId: origin.sessionId,
537
- clientRequestId: origin.clientRequestId,
538
- broker,
539
- outcome,
540
- });
367
+ void this.reportObservation(credential.token, { version: 1, attemptId,
368
+ runtimeOperationId: origin.runtimeOperationId, sessionId: origin.sessionId,
369
+ clientRequestId: origin.clientRequestId, broker, outcome });
541
370
  }
542
371
  }
543
372
  }
544
373
  catch {
545
374
  if (!response.headersSent) {
546
- response.writeHead(503, {
547
- 'content-type': 'application/json',
548
- 'cache-control': 'no-store',
549
- });
550
- response.end(JSON.stringify({
551
- error: { message: 'model_credential_unavailable', type: 'server_error' },
552
- }));
375
+ response.writeHead(503, { 'content-type': 'application/json', 'cache-control': 'no-store' });
376
+ response.end(JSON.stringify({ error: { message: 'model_credential_unavailable', type: 'server_error' } }));
553
377
  }
554
378
  else
555
379
  response.destroy();
556
380
  }
557
381
  finally {
558
- if (!requestEnded)
559
- capturedRequest?.finish({
560
- sourceEnd: abort.signal.aborted ? 'aborted' : 'io_error',
561
- protocolTerminal: 'unknown',
562
- });
563
382
  response.off('close', cancel);
564
383
  this.requests.delete(abort);
565
384
  if (activeRoute) {
@@ -573,17 +392,12 @@ export class WorkspaceModelBroker {
573
392
  async reportObservation(token, input) {
574
393
  try {
575
394
  const response = await fetch(platformEndpoint(this.state.config.publicBaseUrl, '/api/llm-proxy/runtime-observations'), {
576
- method: 'POST',
577
- redirect: 'error',
578
- signal: AbortSignal.timeout(3_000),
579
- headers: { authorization: `Bearer ${token}`, 'content-type': 'application/json' },
580
- body: JSON.stringify(input),
395
+ method: 'POST', redirect: 'error', signal: AbortSignal.timeout(3_000),
396
+ headers: { authorization: `Bearer ${token}`, 'content-type': 'application/json' }, body: JSON.stringify(input),
581
397
  });
582
398
  await response.body?.cancel();
583
399
  }
584
- catch {
585
- /* Missing observations remain unknown; never replay a model request for telemetry. */
586
- }
400
+ catch { /* Missing observations remain unknown; never replay a model request for telemetry. */ }
587
401
  }
588
402
  cancelSession(cwd, sessionId) {
589
403
  const routeKey = `${/^(basic|remix-)/.test(basename(cwd)) ? basename(dirname(cwd)) : basename(cwd)}/${createHash('sha256').update(sessionId).digest('hex').slice(0, 32)}`;
@@ -610,8 +424,7 @@ export class WorkspaceModelBroker {
610
424
  request.abort();
611
425
  if (server) {
612
426
  server.closeAllConnections();
613
- await new Promise((resolve) => server.close(() => resolve()));
427
+ await new Promise(resolve => server.close(() => resolve()));
614
428
  }
615
- await this.capture.close();
616
429
  }
617
430
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@playcraft/cli",
3
- "version": "0.1.85-beta",
3
+ "version": "0.1.86",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "engines": {
@@ -22,7 +22,6 @@
22
22
  "README.md"
23
23
  ],
24
24
  "scripts": {
25
- "lint": "pnpm -w run lint:eslint packages/cli",
26
25
  "dev": "tsc -w",
27
26
  "build": "node scripts/generate-asset-skills.mjs && node scripts/generate-video-skills.mjs && node scripts/generate-parameter-skill.mjs && node scripts/generate-level-editor-skill.mjs && node scripts/generate-preview-runtime.mjs && tsc",
28
27
  "start": "node dist/index.js",
@@ -58,8 +57,8 @@
58
57
  "@gltf-transform/core": "^4.3.0",
59
58
  "@gltf-transform/extensions": "^4.3.0",
60
59
  "@gltf-transform/functions": "^4.3.0",
61
- "@playcraft/build": "0.1.85-beta",
62
- "@playcraft/common": "0.1.85-beta",
60
+ "@playcraft/build": "0.1.86",
61
+ "@playcraft/common": "0.1.86",
63
62
  "better-sqlite3-pinned": "npm:better-sqlite3@13.0.3",
64
63
  "chokidar": "^4.0.3",
65
64
  "commander": "^13.1.0",
@@ -8,7 +8,7 @@
8
8
  "release-guard.mjs": "728ac088f8d885a50a1c9bcdddad724fa80afd06e66a6dd3658756034e563055",
9
9
  "runtime.d.mts": "a90c69a8b72105e8dc8fe0b3bcd272f038ed255f7daf5ec6c1aefc293a1e84b2",
10
10
  "runtime.d.ts": "a90c69a8b72105e8dc8fe0b3bcd272f038ed255f7daf5ec6c1aefc293a1e84b2",
11
- "runtime.mjs": "fefc50ed3246b6d33934179c04952c48f424fcce4d8d45b35bbcd083b644e41e",
11
+ "runtime.mjs": "50dc53ff94effedf10903df708c1ecdb51bd8145a5a6ec1eac9449cad7424a24",
12
12
  "types/adapters.d.ts": "f5e70c4bb9043de1c590eada14266f613286360c809faf26680b0bbbd1cc7b2c",
13
13
  "types/host.d.ts": "9ea867d5485a66aba7da86ea7e5583bca6b9a7a89618afd0eb70ace58e06faf9",
14
14
  "types/index.d.ts": "210d24a767f260bc8944d917b50dae1357cba31b8b5d499bea7bcc99b1051e6c",
@@ -257,7 +257,7 @@ var package_default = {
257
257
  test: "vitest run",
258
258
  "test:watch": "vitest",
259
259
  playground: 'pnpm --filter "@playcraft/iframe-bridge-playground-*" --parallel dev',
260
- lint: "pnpm -w run lint:eslint packages/iframe-bridge",
260
+ lint: "echo 'No eslint config yet'",
261
261
  release: "node ../../scripts/release-npm.mjs --packages=iframe-bridge --skip-install --skip-tests --skip-git-push"
262
262
  },
263
263
  devDependencies: {
@@ -1,10 +0,0 @@
1
- /** Matches the root-owned sandbox launcher: control 1500, development children 1501. */
2
- export function isCaptureIsolationAvailable(input = {
3
- platform: process.platform,
4
- uid: process.getuid?.(),
5
- launcher: process.env.PLAYCRAFT_LAUNCHER_SOCKET,
6
- }) {
7
- return (input.platform === 'linux' &&
8
- input.uid === 1500 &&
9
- input.launcher === '/run/playcraft/runtime/launcher.sock');
10
- }