@playcraft/cli 0.1.57-beta → 0.1.59-beta
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/workspace.js +43 -0
- package/dist/utils/agent-generation-provenance.js +1 -1
- package/dist/workspace/messages.js +10 -0
- package/dist/workspace-runtime/batch/deliver.js +140 -80
- package/dist/workspace-runtime/batch/freeze.js +18 -6
- package/dist/workspace-runtime/batch/generate.js +68 -64
- package/dist/workspace-runtime/batch/policy.js +71 -22
- package/dist/workspace-runtime/batch/prepare.js +108 -93
- package/dist/workspace-runtime/batch/records.js +4 -0
- package/dist/workspace-runtime/batch/routes.js +10 -2
- package/dist/workspace-runtime/batch/source-candidate.js +2 -0
- package/dist/workspace-runtime/batch/source-capture.js +1 -0
- package/dist/workspace-runtime/batch/source-failure.js +2 -0
- package/dist/workspace-runtime/batch/source-publish.js +3 -0
- package/dist/workspace-runtime/batch/writer-proof.js +37 -0
- package/dist/workspace-runtime/codex/app-server.js +278 -20
- package/dist/workspace-runtime/codex/config-toml.js +4 -0
- package/dist/workspace-runtime/codex/delivery-tools.js +1 -1
- package/dist/workspace-runtime/codex/generation-provenance-broker.js +17 -8
- package/dist/workspace-runtime/codex/loopback.js +2 -0
- package/dist/workspace-runtime/codex/model-broker.js +7 -4
- package/dist/workspace-runtime/persistence/journal.js +34 -0
- package/dist/workspace-runtime/persistence/sqlite-worker.js +68 -6
- package/dist/workspace-runtime/previews/output.js +2 -0
- package/dist/workspace-runtime/previews/publish.js +20 -5
- package/dist/workspace-runtime/previews/runtime-preview.js +4 -0
- package/dist/workspace-runtime/processes/managed-writes.js +47 -5
- package/dist/workspace-runtime/serve.js +13 -3
- package/dist/workspace-runtime/server/accept-operation.js +22 -2
- package/dist/workspace-runtime/server/directory-access.js +414 -0
- package/dist/workspace-runtime/server/dispatch.js +74 -29
- package/dist/workspace-runtime/server/endpoints.js +1 -0
- package/dist/workspace-runtime/server/http.js +80 -10
- package/dist/workspace-runtime/server/operation-record.js +2 -0
- package/dist/workspace-runtime/server/terminal-host.js +124 -8
- package/dist/workspace-runtime/server/workspace-storage.js +193 -0
- package/dist/workspace-runtime/server/workspace-transfer.js +3 -1
- package/dist/workspace-runtime/workspaces/append-only-log.js +3 -0
- package/dist/workspace-runtime/workspaces/artifact-events.js +5 -2
- package/dist/workspace-runtime/workspaces/artifact-files.js +14 -8
- package/dist/workspace-runtime/workspaces/artifact-receipts.js +7 -25
- package/dist/workspace-runtime/workspaces/artifacts.js +4 -1
- package/dist/workspace-runtime/workspaces/asset-library-import.js +9 -4
- package/dist/workspace-runtime/workspaces/asset-publication-access.js +7 -9
- package/dist/workspace-runtime/workspaces/asset-transfer-publish.js +3 -0
- package/dist/workspace-runtime/workspaces/asset-transfer-storage.js +17 -2
- package/dist/workspace-runtime/workspaces/attachments.js +9 -0
- package/dist/workspace-runtime/workspaces/batch-commit.js +5 -3
- package/dist/workspace-runtime/workspaces/binary-write.js +12 -7
- package/dist/workspace-runtime/workspaces/cli-context.js +2 -0
- package/dist/workspace-runtime/workspaces/cli-credentials.js +1 -0
- package/dist/workspace-runtime/workspaces/delivery-build.js +30 -11
- package/dist/workspace-runtime/workspaces/delivery-store.js +2 -0
- package/dist/workspace-runtime/workspaces/dependencies.js +90 -70
- package/dist/workspace-runtime/workspaces/developer-command.js +12 -1
- package/dist/workspace-runtime/workspaces/directory-access.js +13 -18
- package/dist/workspace-runtime/workspaces/directory-attachments.js +8 -0
- package/dist/workspace-runtime/workspaces/directory-create.js +18 -5
- package/dist/workspace-runtime/workspaces/directory-maintenance.js +46 -35
- package/dist/workspace-runtime/workspaces/directory-mutations.js +8 -3
- package/dist/workspace-runtime/workspaces/files.js +4 -0
- package/dist/workspace-runtime/workspaces/git-credential-broker.js +3 -0
- package/dist/workspace-runtime/workspaces/git-direct-store.js +2 -0
- package/dist/workspace-runtime/workspaces/json5-edit.js +36 -0
- package/dist/workspace-runtime/workspaces/library-attachments.js +4 -0
- package/dist/workspace-runtime/workspaces/parameter-agent-protocol.js +2 -0
- package/dist/workspace-runtime/workspaces/parameter-agent-store.js +2 -0
- package/dist/workspace-runtime/workspaces/parameter-agent.js +40 -11
- package/dist/workspace-runtime/workspaces/parameter-assets.js +1 -0
- package/dist/workspace-runtime/workspaces/parameter-initialization-skill-transaction.js +141 -0
- package/dist/workspace-runtime/workspaces/parameter-initialization-skill.js +329 -0
- package/dist/workspace-runtime/workspaces/parameter-initialization.js +63 -0
- package/dist/workspace-runtime/workspaces/parameter-production.js +124 -0
- package/dist/workspace-runtime/workspaces/parameters.js +95 -3
- package/dist/workspace-runtime/workspaces/platform-export.js +5 -0
- package/dist/workspace-runtime/workspaces/prepare.js +4 -0
- package/dist/workspace-runtime/workspaces/registry.js +55 -3
- package/dist/workspace-runtime/workspaces/remix-references.js +5 -0
- package/dist/workspace-runtime/workspaces/shared-write.js +187 -0
- package/dist/workspace-runtime/workspaces/skills.js +14 -15
- package/dist/workspace-runtime/workspaces/storage-snapshot.js +235 -0
- package/dist/workspace-runtime/workspaces/transfer.js +4 -1
- package/dist/workspace-runtime/workspaces/workspace-shares.js +235 -0
- package/dist/workspace-runtime/workspaces/worktree-attachments.js +2 -0
- package/dist/workspace-runtime/workspaces/worktree-checkout.js +11 -6
- package/package.json +5 -4
- package/project-template/.claude/skills/playcraft-runtime-parameters/SKILL.md +32 -0
- package/project-template/.claude/skills/playcraft-runtime-parameters/references/platform-protocol.md +21 -0
- package/project-template/.claude/skills/playcraft-runtime-parameters/references/preview-integration.md +57 -0
|
@@ -32,6 +32,10 @@ const ENGINEERING_BATCH_INSTRUCTIONS = [
|
|
|
32
32
|
'The platform freezes the current source and generates each planned variant in its own Remix directory. Submission authorizes automatic generation without another prompt approval. Keep the source unchanged for this batch; do not replace independent variants with theme switching in the source.',
|
|
33
33
|
'Pass --team-id when the trusted batch request context supplies teamId. Creation means queued; use playcraft tools get-batch --batch-id <the actual batch identifier> for results. On an uncertain creation, reuse the same clientRequestId. Report creation failures without falling back to editing the source.',
|
|
34
34
|
].join('\n');
|
|
35
|
+
const MEMBER_EXECUTION_INSTRUCTIONS = [
|
|
36
|
+
'You are implementing one assigned variant in an independent Remix directory which the platform has already created.',
|
|
37
|
+
'Apply the assigned changes directly to the current working directory. The originating source is a separate directory; source freezing, variant planning and directory creation have already been handled by the platform.',
|
|
38
|
+
].join('\n');
|
|
35
39
|
function codexReasoningEffort(effort) {
|
|
36
40
|
return effort === 'max' ? 'xhigh' : effort;
|
|
37
41
|
}
|
|
@@ -40,11 +44,65 @@ function asRecord(value) {
|
|
|
40
44
|
? value
|
|
41
45
|
: {};
|
|
42
46
|
}
|
|
47
|
+
function nativeContextHash(params) {
|
|
48
|
+
return createHash('sha256').update(JSON.stringify(params.developerInstructions ?? '')).digest('hex');
|
|
49
|
+
}
|
|
43
50
|
function isReadOnlySnapshot(value) {
|
|
44
51
|
const settings = asRecord(value);
|
|
45
52
|
const policy = asRecord(settings.sandboxPolicy ?? settings.sandbox);
|
|
46
53
|
return settings.approvalPolicy === 'never' && policy.type === 'readOnly' && policy.networkAccess === false;
|
|
47
54
|
}
|
|
55
|
+
const DEFAULT_PREDICTION_TIMEOUT_MS = 15_000;
|
|
56
|
+
const PREDICTION_INSTRUCTIONS = [
|
|
57
|
+
"You predict the user's most likely next message in this coding session.",
|
|
58
|
+
'Use only the conversation below. Do not answer it, continue the work, or call tools.',
|
|
59
|
+
"Write in the user's language as short imperative requests, at most 80 characters each.",
|
|
60
|
+
'Return JSON only, with no markdown fences, exactly in this shape:',
|
|
61
|
+
'{"suggestions":["...","..."]}',
|
|
62
|
+
'Provide 1 to 3 suggestions ordered by likelihood, without explanations.',
|
|
63
|
+
].join('\n');
|
|
64
|
+
const PREDICTION_OUTPUT_SCHEMA = {
|
|
65
|
+
type: 'object',
|
|
66
|
+
additionalProperties: false,
|
|
67
|
+
required: ['suggestions'],
|
|
68
|
+
properties: {
|
|
69
|
+
suggestions: {
|
|
70
|
+
type: 'array',
|
|
71
|
+
minItems: 1,
|
|
72
|
+
maxItems: 3,
|
|
73
|
+
items: { type: 'string', minLength: 1, maxLength: 160 },
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
/** Parses the structured prediction turn without trusting markdown fences or extra prose. */
|
|
78
|
+
export function parsePredictionSuggestions(text) {
|
|
79
|
+
const cleaned = text.trim().replace(/^```(?:json)?\s*/i, '').replace(/\s*```$/, '').trim();
|
|
80
|
+
const start = cleaned.indexOf('{');
|
|
81
|
+
const end = cleaned.lastIndexOf('}');
|
|
82
|
+
if (start < 0 || end <= start)
|
|
83
|
+
return [];
|
|
84
|
+
let parsed;
|
|
85
|
+
try {
|
|
86
|
+
parsed = JSON.parse(cleaned.slice(start, end + 1));
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
return [];
|
|
90
|
+
}
|
|
91
|
+
const candidates = asRecord(parsed).suggestions;
|
|
92
|
+
if (!Array.isArray(candidates))
|
|
93
|
+
return [];
|
|
94
|
+
return candidates
|
|
95
|
+
.filter((value) => typeof value === 'string')
|
|
96
|
+
.map(value => value.trim())
|
|
97
|
+
.filter(Boolean)
|
|
98
|
+
.slice(0, 3);
|
|
99
|
+
}
|
|
100
|
+
function predictionPrompt(items) {
|
|
101
|
+
const recent = items.slice(-8)
|
|
102
|
+
.map(item => `${item.role.toUpperCase()}: ${item.text.slice(0, 4000)}`)
|
|
103
|
+
.join('\n\n');
|
|
104
|
+
return `${PREDICTION_INSTRUCTIONS}\n\nConversation (oldest first):\n${recent || '(no earlier messages)'}`;
|
|
105
|
+
}
|
|
48
106
|
function waitForChildExit(child, processGroup = false) {
|
|
49
107
|
if ((child.exitCode !== null || child.signalCode !== null) && child.stdout?.destroyed && child.stderr?.destroyed)
|
|
50
108
|
return Promise.resolve();
|
|
@@ -89,6 +147,34 @@ export function createCodexAppServerAdapter(options) {
|
|
|
89
147
|
const threads = new Map();
|
|
90
148
|
const approvals = new Map();
|
|
91
149
|
const childParents = new Map();
|
|
150
|
+
const predictions = new Map();
|
|
151
|
+
const predictionByParent = new Map();
|
|
152
|
+
const predictionTimeoutMs = options.predictionTimeoutMs ?? DEFAULT_PREDICTION_TIMEOUT_MS;
|
|
153
|
+
function settlePrediction(threadId, error) {
|
|
154
|
+
const prediction = predictions.get(threadId);
|
|
155
|
+
if (!prediction || prediction.settled)
|
|
156
|
+
return;
|
|
157
|
+
prediction.settled = true;
|
|
158
|
+
clearTimeout(prediction.timer);
|
|
159
|
+
predictions.delete(threadId);
|
|
160
|
+
if (predictionByParent.get(prediction.parentThreadId) === threadId)
|
|
161
|
+
predictionByParent.delete(prediction.parentThreadId);
|
|
162
|
+
const suggestions = error ? [] : parsePredictionSuggestions(prediction.chunks.join(''));
|
|
163
|
+
prediction.resolve(suggestions.length
|
|
164
|
+
? { outcome: 'ok', suggestions }
|
|
165
|
+
: { outcome: 'failed', error: error ?? 'prediction_empty' });
|
|
166
|
+
}
|
|
167
|
+
async function cancelPredictionForThread(parentThreadId) {
|
|
168
|
+
const threadId = predictionByParent.get(parentThreadId);
|
|
169
|
+
const prediction = threadId ? predictions.get(threadId) : undefined;
|
|
170
|
+
if (!threadId || !prediction || !rpc)
|
|
171
|
+
return;
|
|
172
|
+
const turnId = prediction.turnId;
|
|
173
|
+
settlePrediction(threadId, 'prediction_cancelled');
|
|
174
|
+
if (turnId)
|
|
175
|
+
await rpc.request('turn/interrupt', { threadId, turnId }, 10_000).catch(() => undefined);
|
|
176
|
+
await rpc.request('thread/unsubscribe', { threadId }, 10_000).catch(() => undefined);
|
|
177
|
+
}
|
|
92
178
|
let child;
|
|
93
179
|
let rpc;
|
|
94
180
|
let readyFlag = false;
|
|
@@ -153,9 +239,9 @@ export function createCodexAppServerAdapter(options) {
|
|
|
153
239
|
startPromise ??= start();
|
|
154
240
|
await startPromise;
|
|
155
241
|
}
|
|
156
|
-
async function threadConfig(cwd, modelConfigId, sessionId, reasoningEffort, clientRequestId) {
|
|
242
|
+
async function threadConfig(cwd, modelConfigId, sessionId, reasoningEffort, clientRequestId, originCwd) {
|
|
157
243
|
try {
|
|
158
|
-
return await options.modelBroker?.prepareThreadConfig(cwd, modelConfigId, sessionId, reasoningEffort, clientRequestId)
|
|
244
|
+
return await options.modelBroker?.prepareThreadConfig(cwd, modelConfigId, sessionId, reasoningEffort, clientRequestId, originCwd)
|
|
159
245
|
?? { ...options.fixedThreadConfig, modelConfigId: undefined, activate: () => { } };
|
|
160
246
|
}
|
|
161
247
|
catch (error) {
|
|
@@ -166,7 +252,20 @@ export function createCodexAppServerAdapter(options) {
|
|
|
166
252
|
}
|
|
167
253
|
}
|
|
168
254
|
async function lockedThreadConfig(cwd, selectedConfigId, sessionId, reasoningEffort, clientRequestId, permissionMode = 'auto') {
|
|
169
|
-
const
|
|
255
|
+
const scope = relative(options.config.workspaceRoot, cwd);
|
|
256
|
+
const orchestration = options.config.workspaceMode === 'multi' && /^[0-9a-f-]{36}$/i.test(scope);
|
|
257
|
+
let coordinatorDirectory;
|
|
258
|
+
if (orchestration) {
|
|
259
|
+
const key = JSON.stringify([cwd, sessionId]);
|
|
260
|
+
coordinatorDirectory = coordinatorDirectories.get(key);
|
|
261
|
+
if (!coordinatorDirectory) {
|
|
262
|
+
coordinatorDirectory = await mkdtemp(join(tmpdir(), 'playcraft-batch-coordinator-'));
|
|
263
|
+
// The sandbox launches native commands as its development identity, not the controller.
|
|
264
|
+
await chmod(coordinatorDirectory, 0o777);
|
|
265
|
+
coordinatorDirectories.set(key, coordinatorDirectory);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
const { modelConfigId, activate, ...providedModelConfig } = await threadConfig(cwd, selectedConfigId, sessionId, reasoningEffort, clientRequestId, coordinatorDirectory);
|
|
170
269
|
let mcp;
|
|
171
270
|
if (options.loadMcpConfig) {
|
|
172
271
|
try {
|
|
@@ -183,19 +282,11 @@ export function createCodexAppServerAdapter(options) {
|
|
|
183
282
|
...mcp?.config,
|
|
184
283
|
...(reasoningEffort ? { model_reasoning_effort: codexReasoningEffort(reasoningEffort) } : {}),
|
|
185
284
|
} };
|
|
186
|
-
const
|
|
187
|
-
const
|
|
188
|
-
const engineeringBatch = !options.fixedExecution && options.config.workspaceMode === 'multi'
|
|
285
|
+
const unattended = Boolean(options.fixedExecution || (sessionId && options.trustedExecution?.(sessionId)));
|
|
286
|
+
const engineeringBatch = !unattended && options.config.workspaceMode === 'multi'
|
|
189
287
|
&& /^[0-9a-f-]{36}\/(?:basic|remix-[a-z0-9][a-z0-9-]{3,63})$/i.test(scope);
|
|
190
288
|
if (orchestration) {
|
|
191
|
-
const
|
|
192
|
-
let directory = coordinatorDirectories.get(key);
|
|
193
|
-
if (!directory) {
|
|
194
|
-
directory = await mkdtemp(join(tmpdir(), 'playcraft-batch-coordinator-'));
|
|
195
|
-
// The sandbox launches native commands as its development identity, not the controller.
|
|
196
|
-
await chmod(directory, 0o777);
|
|
197
|
-
coordinatorDirectories.set(key, directory);
|
|
198
|
-
}
|
|
289
|
+
const directory = coordinatorDirectory;
|
|
199
290
|
const execution = executionPolicy(directory, permissionMode, { cwd: directory, approvalPolicy: 'never', sandboxPolicy: {
|
|
200
291
|
type: 'workspaceWrite', writableRoots: [directory], networkAccess: true,
|
|
201
292
|
excludeTmpdirEnvVar: true, excludeSlashTmp: true,
|
|
@@ -205,7 +296,8 @@ export function createCodexAppServerAdapter(options) {
|
|
|
205
296
|
'sandbox_workspace_write.network_access': true,
|
|
206
297
|
'sandbox_workspace_write.exclude_tmpdir_env_var': true,
|
|
207
298
|
'sandbox_workspace_write.exclude_slash_tmp': true,
|
|
208
|
-
'shell_environment_policy.set': {
|
|
299
|
+
'shell_environment_policy.set': { ...asRecord(asRecord(modelConfig.config)['shell_environment_policy.set']),
|
|
300
|
+
PLAYCRAFT_TOOL_REQUEST_DIR: directory, PLAYCRAFT_SKIP_UPDATE_CHECK: '1', TMPDIR: directory }, };
|
|
209
301
|
const developerInstructions = [
|
|
210
302
|
'You coordinate PlayCraft batches. The workspace engineering directories are read-only in this conversation.',
|
|
211
303
|
`Workspace identity: ${scope}. Engineering source root: ${JSON.stringify(cwd)}. Default source directory: basic.`,
|
|
@@ -228,7 +320,7 @@ export function createCodexAppServerAdapter(options) {
|
|
|
228
320
|
const execution = resolveSessionExecution(cwd, permissionMode, sessionId ? options.trustedExecution?.(sessionId) : undefined, options.fixedExecution);
|
|
229
321
|
const selected = threadExecutionParams(execution);
|
|
230
322
|
return { params: { ...modelConfig, ...selected, config: withModelShell({ ...modelConfig.config, ...selected.config }, modelConfigId),
|
|
231
|
-
...(
|
|
323
|
+
...((unattended || options.workspaceUi) ? { developerInstructions: unattended ? MEMBER_EXECUTION_INSTRUCTIONS : WORKSPACE_UI_INSTRUCTIONS } : {}) },
|
|
232
324
|
execution, modelConfigId, activate, skillsHash: undefined, discoveryHash: undefined, mcpHash: mcp?.hash };
|
|
233
325
|
}
|
|
234
326
|
try {
|
|
@@ -249,7 +341,8 @@ export function createCodexAppServerAdapter(options) {
|
|
|
249
341
|
...frozen.skills.map(skill => JSON.stringify(skill)),
|
|
250
342
|
...(frozen.skills.length ? [] : ['(No workspace skills are enabled.)']),
|
|
251
343
|
...(engineeringBatch ? [ENGINEERING_BATCH_INSTRUCTIONS] : []),
|
|
252
|
-
...(
|
|
344
|
+
...(unattended ? [MEMBER_EXECUTION_INSTRUCTIONS] : []),
|
|
345
|
+
...(options.workspaceUi && !unattended ? [WORKSPACE_UI_INSTRUCTIONS] : []),
|
|
253
346
|
].join('\n');
|
|
254
347
|
const discoveryHash = createHash('sha256').update(JSON.stringify(listing.skills.map(skill => skill.path).sort())).digest('hex');
|
|
255
348
|
const execution = resolveSessionExecution(cwd, permissionMode, sessionId ? options.trustedExecution?.(sessionId) : undefined, options.fixedExecution);
|
|
@@ -312,6 +405,7 @@ export function createCodexAppServerAdapter(options) {
|
|
|
312
405
|
async function resumePreparedThread(threadId, thread, prepared) {
|
|
313
406
|
const resumed = await rpc.request('thread/resume', {
|
|
314
407
|
threadId, ...prepared.params,
|
|
408
|
+
...(dynamicTools.length ? { dynamicTools } : {}),
|
|
315
409
|
}, 30_000);
|
|
316
410
|
if (resumed.thread?.id !== threadId)
|
|
317
411
|
throw new NativePreparationError('workspace_skills_unavailable');
|
|
@@ -320,6 +414,7 @@ export function createCodexAppServerAdapter(options) {
|
|
|
320
414
|
thread.skillsHash = prepared.skillsHash;
|
|
321
415
|
thread.discoveryHash = prepared.discoveryHash;
|
|
322
416
|
thread.mcpHash = prepared.mcpHash;
|
|
417
|
+
thread.contextHash = nativeContextHash(prepared.params);
|
|
323
418
|
}
|
|
324
419
|
async function start() {
|
|
325
420
|
if (closed)
|
|
@@ -372,6 +467,7 @@ export function createCodexAppServerAdapter(options) {
|
|
|
372
467
|
}
|
|
373
468
|
else {
|
|
374
469
|
// Local execution owns its fresh profile; managed launcher profiles are prepared for the development identity by the image.
|
|
470
|
+
// private
|
|
375
471
|
await mkdir(homeDir, { recursive: true, mode: 0o700 });
|
|
376
472
|
if (closed)
|
|
377
473
|
throw new Error('codex_not_started');
|
|
@@ -411,6 +507,30 @@ export function createCodexAppServerAdapter(options) {
|
|
|
411
507
|
const nativeClient = rpc;
|
|
412
508
|
rpc.onNotification((method, params) => {
|
|
413
509
|
const payload = asRecord(params);
|
|
510
|
+
const predictionThreadId = typeof payload.threadId === 'string' ? payload.threadId : undefined;
|
|
511
|
+
const prediction = predictionThreadId ? predictions.get(predictionThreadId) : undefined;
|
|
512
|
+
if (prediction && predictionThreadId) {
|
|
513
|
+
if (method === 'turn/started') {
|
|
514
|
+
const turnId = asRecord(payload.turn).id;
|
|
515
|
+
if (typeof turnId === 'string')
|
|
516
|
+
prediction.turnId = turnId;
|
|
517
|
+
}
|
|
518
|
+
else if (method === 'item/agentMessage/delta' && typeof payload.delta === 'string') {
|
|
519
|
+
prediction.chunks.push(payload.delta);
|
|
520
|
+
}
|
|
521
|
+
else if (method === 'item/completed') {
|
|
522
|
+
const message = asRecord(payload.item);
|
|
523
|
+
if (message.type === 'agentMessage' && typeof message.text === 'string' && !prediction.chunks.length)
|
|
524
|
+
prediction.chunks.push(message.text);
|
|
525
|
+
}
|
|
526
|
+
else if (method === 'turn/completed') {
|
|
527
|
+
settlePrediction(predictionThreadId);
|
|
528
|
+
}
|
|
529
|
+
else if (method === 'error') {
|
|
530
|
+
settlePrediction(predictionThreadId, 'prediction_failed');
|
|
531
|
+
}
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
414
534
|
if (method === 'turn/started' && typeof payload.threadId === 'string') {
|
|
415
535
|
const thread = threads.get(payload.threadId), turnId = asRecord(payload.turn).id;
|
|
416
536
|
if (thread?.turnActive && typeof turnId === 'string')
|
|
@@ -478,6 +598,15 @@ export function createCodexAppServerAdapter(options) {
|
|
|
478
598
|
});
|
|
479
599
|
rpc.onServerRequest((id, method, params) => {
|
|
480
600
|
const payload = asRecord(params);
|
|
601
|
+
const prediction = typeof payload.threadId === 'string' ? predictions.get(payload.threadId) : undefined;
|
|
602
|
+
if (prediction) {
|
|
603
|
+
const threadId = String(payload.threadId);
|
|
604
|
+
const turnId = prediction.turnId;
|
|
605
|
+
settlePrediction(threadId, 'prediction_tool_request');
|
|
606
|
+
if (turnId)
|
|
607
|
+
void nativeClient.request('turn/interrupt', { threadId, turnId }, 10_000).catch(() => undefined);
|
|
608
|
+
return;
|
|
609
|
+
}
|
|
481
610
|
if (method === 'item/tool/call') {
|
|
482
611
|
const thread = typeof payload.threadId === 'string' ? threads.get(payload.threadId) : undefined;
|
|
483
612
|
if (deliveryTools && isWorkspaceDeliveryTool(payload.tool)) {
|
|
@@ -701,7 +830,7 @@ export function createCodexAppServerAdapter(options) {
|
|
|
701
830
|
throw new Error('native_receipt_lost');
|
|
702
831
|
prepared.activate();
|
|
703
832
|
threads.set(threadId, { sessionId: input.sessionId, cwd: input.cwd, skillsHash: prepared.skillsHash,
|
|
704
|
-
discoveryHash: prepared.discoveryHash, mcpHash: prepared.mcpHash, resolvedModel: 'model' in prepared.params ? prepared.params.model : model, modelConfigId: prepared.modelConfigId, reasoningEffort: input.reasoningEffort, permissionMode: input.permissionMode ?? 'auto', canEscalate: sessionCanEscalate(Boolean(options.trustedExecution?.(input.sessionId)), prepared.execution, Boolean(options.fixedExecution)), hasNativeHistory: false, loaded: true });
|
|
833
|
+
discoveryHash: prepared.discoveryHash, mcpHash: prepared.mcpHash, contextHash: nativeContextHash(prepared.params), resolvedModel: 'model' in prepared.params ? prepared.params.model : model, modelConfigId: prepared.modelConfigId, reasoningEffort: input.reasoningEffort, permissionMode: input.permissionMode ?? 'auto', canEscalate: sessionCanEscalate(Boolean(options.trustedExecution?.(input.sessionId)), prepared.execution, Boolean(options.fixedExecution)), hasNativeHistory: false, loaded: true });
|
|
705
834
|
threads.get(threadId).readOnlyConfirmed = isReadOnlySnapshot(result);
|
|
706
835
|
return { threadId, modelConfigId: prepared.modelConfigId, reasoningEffort: input.reasoningEffort, permissionMode: input.permissionMode ?? 'auto' };
|
|
707
836
|
});
|
|
@@ -730,7 +859,7 @@ export function createCodexAppServerAdapter(options) {
|
|
|
730
859
|
return undefined;
|
|
731
860
|
prepared.activate();
|
|
732
861
|
threads.set(input.threadId, { sessionId: input.sessionId, cwd: input.cwd, skillsHash: prepared.skillsHash,
|
|
733
|
-
discoveryHash: prepared.discoveryHash, mcpHash: prepared.mcpHash, resolvedModel: 'model' in prepared.params ? prepared.params.model : model, modelConfigId: prepared.modelConfigId, reasoningEffort: input.reasoningEffort, permissionMode: input.permissionMode ?? 'auto', canEscalate: sessionCanEscalate(Boolean(options.trustedExecution?.(input.sessionId)), prepared.execution, Boolean(options.fixedExecution)), hasNativeHistory: true, loaded: true });
|
|
862
|
+
discoveryHash: prepared.discoveryHash, mcpHash: prepared.mcpHash, contextHash: nativeContextHash(prepared.params), resolvedModel: 'model' in prepared.params ? prepared.params.model : model, modelConfigId: prepared.modelConfigId, reasoningEffort: input.reasoningEffort, permissionMode: input.permissionMode ?? 'auto', canEscalate: sessionCanEscalate(Boolean(options.trustedExecution?.(input.sessionId)), prepared.execution, Boolean(options.fixedExecution)), hasNativeHistory: true, loaded: true });
|
|
734
863
|
threads.get(input.threadId).readOnlyConfirmed = readOnlyConfirmed;
|
|
735
864
|
return { modelConfigId: prepared.modelConfigId, reasoningEffort: input.reasoningEffort, permissionMode: input.permissionMode ?? 'auto' };
|
|
736
865
|
});
|
|
@@ -740,6 +869,8 @@ export function createCodexAppServerAdapter(options) {
|
|
|
740
869
|
await ensureStarted();
|
|
741
870
|
if (!rpc)
|
|
742
871
|
throw new Error('codex_not_started');
|
|
872
|
+
// A real turn always wins over a speculative prediction for the same thread.
|
|
873
|
+
await cancelPredictionForThread(input.threadId);
|
|
743
874
|
if (threads.get(input.threadId)?.sessionId !== input.sessionId) {
|
|
744
875
|
return { outcome: 'failed', error: 'unknown_thread' };
|
|
745
876
|
}
|
|
@@ -776,6 +907,12 @@ export function createCodexAppServerAdapter(options) {
|
|
|
776
907
|
else if (mcpChanged || (thread.skillsHash && (prepared.skillsHash !== thread.skillsHash || prepared.discoveryHash !== thread.discoveryHash))) {
|
|
777
908
|
await restartForSkills(input.threadId, prepared.modelConfigId, reasoningEffort, mcpChanged ? 'mcp' : 'skills', mcpChanged ? prepared : undefined);
|
|
778
909
|
}
|
|
910
|
+
if (thread.loaded && thread.contextHash !== nativeContextHash(prepared.params)) {
|
|
911
|
+
if (!thread.hasNativeHistory)
|
|
912
|
+
return { outcome: 'failed', error: 'unknown_thread' };
|
|
913
|
+
await rpc.request('thread/unsubscribe', { threadId: input.threadId }, 30_000);
|
|
914
|
+
thread.loaded = false;
|
|
915
|
+
}
|
|
779
916
|
if (!thread.loaded)
|
|
780
917
|
await resumePreparedThread(input.threadId, thread, prepared);
|
|
781
918
|
thread.skillsHash = prepared.skillsHash;
|
|
@@ -831,6 +968,122 @@ export function createCodexAppServerAdapter(options) {
|
|
|
831
968
|
}
|
|
832
969
|
});
|
|
833
970
|
},
|
|
971
|
+
async startPrediction(input) {
|
|
972
|
+
await ensureStarted();
|
|
973
|
+
if (!rpc)
|
|
974
|
+
return { outcome: 'failed', error: 'prediction_unavailable' };
|
|
975
|
+
const reader = rpc;
|
|
976
|
+
const parent = threads.get(input.threadId);
|
|
977
|
+
if (!parent || parent.sessionId !== input.sessionId)
|
|
978
|
+
return { outcome: 'failed', error: 'unknown_thread' };
|
|
979
|
+
if (parent.turnActive)
|
|
980
|
+
return { outcome: 'failed', error: 'turn_already_active' };
|
|
981
|
+
if (predictionByParent.has(input.threadId))
|
|
982
|
+
await cancelPredictionForThread(input.threadId);
|
|
983
|
+
let items = [];
|
|
984
|
+
try {
|
|
985
|
+
// Recent turns only: full thread/read hydration is deprecated for paginated threads
|
|
986
|
+
// and grows with the whole session. The summary view still carries user/agent text.
|
|
987
|
+
const page = await reader.request('thread/turns/list', { threadId: input.threadId, limit: 4 }, 15_000);
|
|
988
|
+
for (const turn of [...(page.data ?? [])].reverse()) {
|
|
989
|
+
for (const rawItem of turn.items ?? []) {
|
|
990
|
+
const item = asRecord(rawItem);
|
|
991
|
+
if (item.type === 'userMessage') {
|
|
992
|
+
const text = (Array.isArray(item.content) ? item.content : [])
|
|
993
|
+
.map(part => asRecord(part))
|
|
994
|
+
.filter(part => part.type === 'text' && typeof part.text === 'string')
|
|
995
|
+
.map(part => String(part.text)).join('\n').trim();
|
|
996
|
+
if (text)
|
|
997
|
+
items.push({ role: 'user', text });
|
|
998
|
+
}
|
|
999
|
+
else if (item.type === 'agentMessage' && typeof item.text === 'string' && item.text.trim()) {
|
|
1000
|
+
items.push({ role: 'assistant', text: item.text });
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
items = items.slice(-8);
|
|
1005
|
+
}
|
|
1006
|
+
catch {
|
|
1007
|
+
// Older builds without paginated turn listing: fall back to the whole thread.
|
|
1008
|
+
try {
|
|
1009
|
+
const read = await reader.request('thread/read', { threadId: input.threadId, includeTurns: true }, 15_000);
|
|
1010
|
+
items = projectNativeThread(input.threadId, read.thread ?? read)?.items ?? [];
|
|
1011
|
+
}
|
|
1012
|
+
catch {
|
|
1013
|
+
// An empty transcript still lets the model propose generic next steps.
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
let forkId;
|
|
1017
|
+
try {
|
|
1018
|
+
const forked = await withPreparation(async () => {
|
|
1019
|
+
await ensureStarted();
|
|
1020
|
+
if (!rpc)
|
|
1021
|
+
throw new Error('codex_not_started');
|
|
1022
|
+
// Prediction forks cannot combine with config overrides; they inherit the
|
|
1023
|
+
// parent thread's model provider, i.e. the workspace broker route.
|
|
1024
|
+
return await rpc.request('thread/fork', {
|
|
1025
|
+
threadId: input.threadId,
|
|
1026
|
+
ephemeral: true,
|
|
1027
|
+
excludeTurns: true,
|
|
1028
|
+
threadSource: 'composer_predictions',
|
|
1029
|
+
experimentalPredictionMode: true,
|
|
1030
|
+
}, 30_000);
|
|
1031
|
+
});
|
|
1032
|
+
forkId = forked.thread?.id;
|
|
1033
|
+
}
|
|
1034
|
+
catch {
|
|
1035
|
+
return { outcome: 'failed', error: 'prediction_unavailable' };
|
|
1036
|
+
}
|
|
1037
|
+
if (!forkId)
|
|
1038
|
+
return { outcome: 'failed', error: 'prediction_unavailable' };
|
|
1039
|
+
const predictionThreadId = forkId;
|
|
1040
|
+
const client = rpc;
|
|
1041
|
+
if (!client)
|
|
1042
|
+
return { outcome: 'failed', error: 'prediction_unavailable' };
|
|
1043
|
+
childParents.set(predictionThreadId, input.threadId);
|
|
1044
|
+
const result = new Promise((resolve) => {
|
|
1045
|
+
const timer = setTimeout(() => {
|
|
1046
|
+
const pending = predictions.get(predictionThreadId);
|
|
1047
|
+
if (pending?.turnId)
|
|
1048
|
+
void client.request('turn/interrupt', {
|
|
1049
|
+
threadId: predictionThreadId, turnId: pending.turnId,
|
|
1050
|
+
}, 10_000).catch(() => undefined);
|
|
1051
|
+
settlePrediction(predictionThreadId, 'prediction_timeout');
|
|
1052
|
+
}, predictionTimeoutMs);
|
|
1053
|
+
timer.unref?.();
|
|
1054
|
+
predictions.set(predictionThreadId, {
|
|
1055
|
+
parentThreadId: input.threadId, chunks: [], settled: false, resolve, timer,
|
|
1056
|
+
});
|
|
1057
|
+
predictionByParent.set(input.threadId, predictionThreadId);
|
|
1058
|
+
});
|
|
1059
|
+
try {
|
|
1060
|
+
const started = await client.request('turn/start', {
|
|
1061
|
+
threadId: predictionThreadId,
|
|
1062
|
+
input: [{ type: 'text', text: predictionPrompt(items) }],
|
|
1063
|
+
outputSchema: PREDICTION_OUTPUT_SCHEMA,
|
|
1064
|
+
turnTrigger: 'prediction',
|
|
1065
|
+
clientUserMessageId: `prediction_${randomUUID()}`,
|
|
1066
|
+
approvalPolicy: 'never',
|
|
1067
|
+
sandboxPolicy: { type: 'readOnly', networkAccess: false },
|
|
1068
|
+
}, 30_000);
|
|
1069
|
+
const pending = predictions.get(predictionThreadId);
|
|
1070
|
+
if (pending && started.turn?.id)
|
|
1071
|
+
pending.turnId = started.turn.id;
|
|
1072
|
+
}
|
|
1073
|
+
catch {
|
|
1074
|
+
settlePrediction(predictionThreadId, 'prediction_unavailable');
|
|
1075
|
+
}
|
|
1076
|
+
try {
|
|
1077
|
+
return await result;
|
|
1078
|
+
}
|
|
1079
|
+
finally {
|
|
1080
|
+
predictions.delete(predictionThreadId);
|
|
1081
|
+
if (predictionByParent.get(input.threadId) === predictionThreadId)
|
|
1082
|
+
predictionByParent.delete(input.threadId);
|
|
1083
|
+
childParents.delete(predictionThreadId);
|
|
1084
|
+
await client.request('thread/unsubscribe', { threadId: predictionThreadId }, 10_000).catch(() => undefined);
|
|
1085
|
+
}
|
|
1086
|
+
},
|
|
834
1087
|
async startReview(input) {
|
|
835
1088
|
return withPreparation(async () => {
|
|
836
1089
|
await ensureStarted();
|
|
@@ -972,6 +1225,7 @@ export function createCodexAppServerAdapter(options) {
|
|
|
972
1225
|
},
|
|
973
1226
|
async interrupt(input) {
|
|
974
1227
|
await ensureStarted();
|
|
1228
|
+
await cancelPredictionForThread(input.threadId);
|
|
975
1229
|
const thread = threads.get(input.threadId);
|
|
976
1230
|
// Nothing is sent when no turn of this session runs here, so that answer is definite.
|
|
977
1231
|
if (thread?.sessionId !== input.sessionId || !thread.turnActive)
|
|
@@ -982,11 +1236,13 @@ export function createCodexAppServerAdapter(options) {
|
|
|
982
1236
|
// A turn that is still starting has no id yet; the last one is already over.
|
|
983
1237
|
if (!turnId || turnId === thread.lastCompletedTurnId)
|
|
984
1238
|
return { outcome: 'uncertain' };
|
|
1239
|
+
if (input.expectedTurnId !== undefined && input.expectedTurnId !== turnId)
|
|
1240
|
+
return { outcome: 'failed', error: 'turn_changed' };
|
|
985
1241
|
try {
|
|
986
1242
|
await uiTools.cancel(input.threadId);
|
|
987
1243
|
await rpc.request('turn/interrupt', { threadId: input.threadId, turnId });
|
|
988
1244
|
const cwd = threads.get(input.threadId)?.cwd;
|
|
989
|
-
if (cwd)
|
|
1245
|
+
if (cwd && (input.expectedTurnId === undefined || threads.get(input.threadId)?.lastTurnId === turnId))
|
|
990
1246
|
options.modelBroker?.cancelSession(cwd, input.sessionId);
|
|
991
1247
|
}
|
|
992
1248
|
catch {
|
|
@@ -1039,6 +1295,8 @@ export function createCodexAppServerAdapter(options) {
|
|
|
1039
1295
|
},
|
|
1040
1296
|
close() {
|
|
1041
1297
|
closing ??= (async () => {
|
|
1298
|
+
for (const threadId of [...predictions.keys()])
|
|
1299
|
+
settlePrediction(threadId, 'prediction_unavailable');
|
|
1042
1300
|
closed = true;
|
|
1043
1301
|
readyFlag = false;
|
|
1044
1302
|
await uiTools.cancel(undefined, 'ui_runtime_disconnected');
|
|
@@ -77,7 +77,9 @@ export async function writePortableCodexCatalog(input) {
|
|
|
77
77
|
}
|
|
78
78
|
await mkdir(input.directory, { recursive: true });
|
|
79
79
|
const path = join(input.directory, 'portable-models.json');
|
|
80
|
+
// private
|
|
80
81
|
await writeFile(path, body, { encoding: 'utf8', mode: 0o644 });
|
|
82
|
+
// private
|
|
81
83
|
await writeFile(join(input.directory, 'portable-models.version.json'), JSON.stringify({
|
|
82
84
|
version,
|
|
83
85
|
sha256: createHash('sha256').update(body).digest('hex'),
|
|
@@ -85,6 +87,7 @@ export async function writePortableCodexCatalog(input) {
|
|
|
85
87
|
return path;
|
|
86
88
|
}
|
|
87
89
|
export async function writePlaycraftCodexConfig(input) {
|
|
90
|
+
// private
|
|
88
91
|
await mkdir(input.homeDir, { recursive: true, mode: 0o700 });
|
|
89
92
|
const path = join(input.homeDir, 'config.toml');
|
|
90
93
|
const body = [
|
|
@@ -102,6 +105,7 @@ export async function writePlaycraftCodexConfig(input) {
|
|
|
102
105
|
'requires_openai_auth = false',
|
|
103
106
|
'',
|
|
104
107
|
].join('\n');
|
|
108
|
+
// private
|
|
105
109
|
await writeFile(path, body, { encoding: 'utf8', mode: 0o600 });
|
|
106
110
|
return path;
|
|
107
111
|
}
|
|
@@ -5,7 +5,7 @@ export const WORKSPACE_DELIVERY_TOOLS = [
|
|
|
5
5
|
{
|
|
6
6
|
type: 'function',
|
|
7
7
|
name: 'playcraft_build_delivery',
|
|
8
|
-
description: 'Export channel packages through the PlayCraft platform. This first saves the workspace (commit and push), then exports that commit; each channel reuses the options of its latest export on this branch, or the defaults when it has none. A workspace without a project first gets one, with a private repository, as its first save would.
|
|
8
|
+
description: 'Export channel packages through the PlayCraft platform. This first saves the workspace (commit and push), then exports that commit; each channel reuses the options of its latest export on this branch, or the defaults when it has none. A workspace without a project first gets one, with a private repository, as its first save would. If the result is pending at workspace.save, finish the current turn without polling so the save can run, then continue the export in a later turn. Once build ids are returned, poll playcraft_get_delivery for status, checks and failure details.',
|
|
9
9
|
inputSchema: {
|
|
10
10
|
type: 'object',
|
|
11
11
|
properties: {
|
|
@@ -13,10 +13,16 @@ export class WorkspaceGenerationProvenanceBroker {
|
|
|
13
13
|
const root = this.state.native?.executionProcess?.();
|
|
14
14
|
return socketPath && root ? prepareLauncherOrigin({ socketPath, rootPid: root.pid, runtimeEpoch: this.state.runtimeEpoch }) : undefined;
|
|
15
15
|
}
|
|
16
|
-
operation(context) {
|
|
17
|
-
|
|
16
|
+
operation(context, batch = false) {
|
|
17
|
+
let active = this.state.journal.listOperations().map(item => item.operation).filter(operation => ['session.turn', 'session.review', 'session.compact'].includes(operation.kind)
|
|
18
18
|
&& ['dispatched', 'accepted', 'uncertain'].includes(operation.status)
|
|
19
19
|
&& operation.generation === this.state.config.generation && operation.runtimeEpoch === this.state.runtimeEpoch);
|
|
20
|
+
if (batch) {
|
|
21
|
+
const owned = active.find(operation => operation.sessionId === context.sessionId && operation.clientRequestId === context.clientRequestId);
|
|
22
|
+
if (!owned)
|
|
23
|
+
return undefined;
|
|
24
|
+
active = active.filter(operation => operation.workspaceId === owned.workspaceId && operation.directoryName === owned.directoryName);
|
|
25
|
+
}
|
|
20
26
|
if (active.length !== 1)
|
|
21
27
|
return undefined;
|
|
22
28
|
const operation = active[0];
|
|
@@ -32,8 +38,7 @@ export class WorkspaceGenerationProvenanceBroker {
|
|
|
32
38
|
const socketPath = process.env.PLAYCRAFT_LAUNCHER_SOCKET;
|
|
33
39
|
const deny = () => { response.writeHead(403, { 'cache-control': 'no-store' }); response.end(); };
|
|
34
40
|
try {
|
|
35
|
-
|
|
36
|
-
if (!valid() || !socketPath || !context.boundaryId || !operation || request.socket.remoteAddress !== '127.0.0.1'
|
|
41
|
+
if (!valid() || !socketPath || !context.boundaryId || request.socket.remoteAddress !== '127.0.0.1'
|
|
37
42
|
|| !request.socket.localPort || !request.socket.remotePort)
|
|
38
43
|
return deny();
|
|
39
44
|
let text = '';
|
|
@@ -43,11 +48,15 @@ export class WorkspaceGenerationProvenanceBroker {
|
|
|
43
48
|
return deny();
|
|
44
49
|
}
|
|
45
50
|
const parsed = requestSchema.safeParse(JSON.parse(text));
|
|
46
|
-
if (!parsed.success
|
|
47
|
-
|
|
51
|
+
if (!parsed.success)
|
|
52
|
+
return deny();
|
|
53
|
+
const batch = parsed.data.toolPath === '/agent/tools/create-batch';
|
|
54
|
+
const operation = this.operation(context, batch);
|
|
55
|
+
if (!operation || !await verifyLauncherOrigin({ socketPath, boundaryId: context.boundaryId,
|
|
56
|
+
runtimeEpoch: this.state.runtimeEpoch, serverPort: request.socket.localPort, clientPort: request.socket.remotePort, cwd: context.originCwd ?? context.cwd }))
|
|
48
57
|
return deny();
|
|
49
58
|
// The socket lookup may take time. Never attest a turn which ended or changed meanwhile.
|
|
50
|
-
if (!valid() || this.operation(context)?.operationId !== operation.operationId)
|
|
59
|
+
if (!valid() || this.operation(context, batch)?.operationId !== operation.operationId)
|
|
51
60
|
return deny();
|
|
52
61
|
const config = this.state.config;
|
|
53
62
|
const input = agentGenerationProvenanceRequestSchema.parse({ ...parsed.data,
|
|
@@ -65,7 +74,7 @@ export class WorkspaceGenerationProvenanceBroker {
|
|
|
65
74
|
return deny();
|
|
66
75
|
}
|
|
67
76
|
const value = z.object({ proof: z.string().min(1).max(8192) }).strict().parse(await upstream.json());
|
|
68
|
-
if (!valid() || this.operation(context)?.operationId !== operation.operationId)
|
|
77
|
+
if (!valid() || this.operation(context, batch)?.operationId !== operation.operationId)
|
|
69
78
|
return deny();
|
|
70
79
|
response.writeHead(200, { 'content-type': 'application/json', 'cache-control': 'no-store' });
|
|
71
80
|
response.end(JSON.stringify(value));
|
|
@@ -87,6 +87,8 @@ export function createLoopbackNativeAdapter() {
|
|
|
87
87
|
if (!thread || thread.sessionId !== input.sessionId)
|
|
88
88
|
return { outcome: 'uncertain' };
|
|
89
89
|
const turnId = thread.turnId;
|
|
90
|
+
if (input.expectedTurnId !== undefined && input.expectedTurnId !== turnId)
|
|
91
|
+
return { outcome: 'failed', error: 'turn_changed' };
|
|
90
92
|
queueMicrotask(() => {
|
|
91
93
|
emit(collectedEvent('notification', 'turn/completed', {
|
|
92
94
|
threadId: input.threadId,
|
|
@@ -52,7 +52,7 @@ export class WorkspaceModelBroker {
|
|
|
52
52
|
activate();
|
|
53
53
|
return settings;
|
|
54
54
|
}
|
|
55
|
-
async prepareThreadConfig(cwd, modelConfigId, sessionId, reasoningEffort, clientRequestId) {
|
|
55
|
+
async prepareThreadConfig(cwd, modelConfigId, sessionId, reasoningEffort, clientRequestId, originCwd) {
|
|
56
56
|
if (!this.server)
|
|
57
57
|
throw new Error('workspace_not_registered');
|
|
58
58
|
const { workspaceId } = await workspaceContextFromRoot(this.state, cwd);
|
|
@@ -61,11 +61,12 @@ export class WorkspaceModelBroker {
|
|
|
61
61
|
const routeKey = sessionId ? `${workspaceId}/${createHash('sha256').update(sessionId).digest('hex').slice(0, 32)}` : workspaceId;
|
|
62
62
|
let route = this.routes.get(routeKey);
|
|
63
63
|
const selected = modelConfigId ?? (route?.epoch === this.state.runtimeEpoch ? route.selected?.entry.modelConfigId : undefined);
|
|
64
|
-
const cacheKey = `${workspaceId}:${selected ?? 'default'}:${reasoningEffort ?? 'default'}`;
|
|
64
|
+
const cacheKey = `${workspaceId}:${selected ?? 'default'}:${reasoningEffort ?? 'default'}:${sessionId ?? ''}:${clientRequestId ?? ''}`;
|
|
65
65
|
let entry = this.entries.get(cacheKey) ?? [...this.entries.values()].find(item => item.workspaceId === workspaceId && item.epoch === this.state.runtimeEpoch && item.modelConfigId === selected
|
|
66
|
+
&& item.sessionId === sessionId && item.clientRequestId === clientRequestId
|
|
66
67
|
&& item.reasoningEffort === reasoningEffort);
|
|
67
68
|
if (!entry || entry.epoch !== this.state.runtimeEpoch) {
|
|
68
|
-
entry = { workspaceId, epoch: this.state.runtimeEpoch, cacheKey, modelConfigId: selected, reasoningEffort };
|
|
69
|
+
entry = { workspaceId, epoch: this.state.runtimeEpoch, cacheKey, modelConfigId: selected, reasoningEffort, sessionId, clientRequestId };
|
|
69
70
|
this.entries.set(cacheKey, entry);
|
|
70
71
|
}
|
|
71
72
|
// Resolve current model metadata and permission at every native turn boundary.
|
|
@@ -82,7 +83,7 @@ export class WorkspaceModelBroker {
|
|
|
82
83
|
if (!this.valid(entry) || this.routes.get(routeKey) !== route)
|
|
83
84
|
throw new Error('model_access_denied');
|
|
84
85
|
route.selected = { entry, model: credential.model,
|
|
85
|
-
...(sessionId && clientRequestId ? { feedback: { sessionId, clientRequestId, cwd, boundaryId } } : {}) };
|
|
86
|
+
...(sessionId && clientRequestId ? { feedback: { sessionId, clientRequestId, cwd, boundaryId, ...(originCwd ? { originCwd } : {}) } } : {}) };
|
|
86
87
|
},
|
|
87
88
|
model: credential.model,
|
|
88
89
|
modelConfigId: credential.configId,
|
|
@@ -125,6 +126,8 @@ export class WorkspaceModelBroker {
|
|
|
125
126
|
headers: { authorization: `Bearer ${config.controlToken}`, 'content-type': 'application/json', 'playcraft-env': config.environment },
|
|
126
127
|
body: JSON.stringify({ runtimeId: config.runtimeId, generation: config.generation,
|
|
127
128
|
runtimeEpoch: entry.epoch, workspaceId: entry.workspaceId,
|
|
129
|
+
...(entry.sessionId ? { sessionId: entry.sessionId } : {}),
|
|
130
|
+
...(entry.clientRequestId ? { clientRequestId: entry.clientRequestId } : {}),
|
|
128
131
|
...(entry.modelConfigId ? { modelConfigId: entry.modelConfigId } : {}),
|
|
129
132
|
...(entry.reasoningEffort ? { reasoningEffort: entry.reasoningEffort } : {}) }),
|
|
130
133
|
});
|