@playcraft/cli 0.1.48-beta → 0.1.50-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/workspace-runtime/batch/deliver.js +105 -87
- package/dist/workspace-runtime/batch/prepare.js +169 -56
- package/dist/workspace-runtime/batch/source-capture.js +7 -20
- package/dist/workspace-runtime/batch/source-publish.js +2 -4
- package/dist/workspace-runtime/codex/app-server.js +19 -1
- package/dist/workspace-runtime/codex/delivery-tools.js +144 -0
- package/dist/workspace-runtime/codex/tool-response.js +10 -0
- package/dist/workspace-runtime/codex/ui-tools.js +22 -5
- package/dist/workspace-runtime/previews/diagnostics.js +44 -0
- package/dist/workspace-runtime/previews/publish.js +29 -8
- package/dist/workspace-runtime/previews/runtime-preview.js +177 -0
- package/dist/workspace-runtime/serve.js +162 -4
- package/dist/workspace-runtime/server/accept-operation.js +61 -0
- package/dist/workspace-runtime/server/asset-transfers.js +63 -0
- package/dist/workspace-runtime/server/dispatch.js +328 -44
- package/dist/workspace-runtime/server/http.js +193 -137
- package/dist/workspace-runtime/server/operation-record.js +33 -0
- package/dist/workspace-runtime/server/submit-operation.js +41 -0
- package/dist/workspace-runtime/workspaces/append-only-log.js +62 -0
- package/dist/workspace-runtime/workspaces/asset-transfer-dependencies.js +81 -0
- package/dist/workspace-runtime/workspaces/asset-transfer-publish.js +434 -0
- package/dist/workspace-runtime/workspaces/asset-transfer-storage.js +389 -0
- package/dist/workspace-runtime/workspaces/delivery-build.js +331 -0
- package/dist/workspace-runtime/workspaces/delivery-store.js +168 -0
- package/dist/workspace-runtime/workspaces/delivery.js +64 -0
- package/dist/workspace-runtime/workspaces/dependencies.js +15 -0
- package/dist/workspace-runtime/workspaces/developer-command.js +35 -1
- package/dist/workspace-runtime/workspaces/directories.js +9 -4
- package/dist/workspace-runtime/workspaces/directory-mutations.js +238 -0
- package/dist/workspace-runtime/workspaces/directory-revisions.js +56 -0
- package/dist/workspace-runtime/workspaces/disk-snapshot.js +3 -3
- package/dist/workspace-runtime/workspaces/json5-edit.js +144 -1
- package/dist/workspace-runtime/workspaces/operation-log.js +23 -0
- package/dist/workspace-runtime/workspaces/parameter-agent-inputs.js +87 -0
- package/dist/workspace-runtime/workspaces/parameter-agent-protocol.js +29 -0
- package/dist/workspace-runtime/workspaces/parameter-agent-store.js +58 -0
- package/dist/workspace-runtime/workspaces/parameter-agent.js +282 -0
- package/dist/workspace-runtime/workspaces/parameters.js +77 -54
- package/package.json +3 -3
|
@@ -4,7 +4,7 @@ import { abandonTurnChanges, beginTurnChanges, finishTurnChanges } from '../work
|
|
|
4
4
|
import { shareWorkspaceGit } from '../workspaces/shared-git.js';
|
|
5
5
|
import { join } from 'node:path';
|
|
6
6
|
import { z } from 'zod';
|
|
7
|
-
import { fileWritePayloadSchema, fileBinaryWritePayloadSchema, directoryCreatePayloadSchema, gitCommitPayloadSchema, gitPathsPayloadSchema, gitPushPayloadSchema, gitPullPayloadSchema, gitMergeAbortPayloadSchema, parameterPatchPayloadSchema, productionSessionAttemptId, reasoningEffortSchema, workspacePermissionModeSchema, workspaceComposerSelectionSchema, workspaceReviewPayloadSchema, workspaceCompactPayloadSchema, workspaceSteerPayloadSchema, workspaceApprovalResponseSchema, terminalCreatePayloadSchema, terminalProcessIdentity, transitionOperation, recoverUncertainOperation, workspaceSavePayloadSchema, workspaceSkillsUpdatePayloadSchema, workspaceRemixReferenceImportPayloadSchema, } from '@playcraft/common/cloud-workspace/runtime';
|
|
7
|
+
import { fileWritePayloadSchema, fileBinaryWritePayloadSchema, directoryCreatePayloadSchema, directoryRenamePayloadSchema, directoryDeletePayloadSchema, directoryMutationReceiptSchema, gitCommitPayloadSchema, gitPathsPayloadSchema, gitPushPayloadSchema, gitPullPayloadSchema, gitMergeAbortPayloadSchema, parameterPatchPayloadSchema, parameterAgentTaskRequestSchema, parameterAgentVerifyPayloadSchema, productionSessionAttemptId, reasoningEffortSchema, workspacePermissionModeSchema, workspaceComposerSelectionSchema, workspaceReviewPayloadSchema, workspaceCompactPayloadSchema, workspaceSteerPayloadSchema, workspaceApprovalResponseSchema, terminalCreatePayloadSchema, terminalProcessIdentity, transitionOperation, recoverUncertainOperation, workspaceSavePayloadSchema, workspaceSkillsUpdatePayloadSchema, workspaceRemixReferenceImportPayloadSchema, deliveryBuildPayloadSchema, deliveryCancelPayloadSchema, } from '@playcraft/common/cloud-workspace/runtime';
|
|
8
8
|
import { closeCliCredentials, ensureCliCredentials } from '../workspaces/cli-credentials.js';
|
|
9
9
|
import { artifactFormat } from '../workspaces/artifact-files.js';
|
|
10
10
|
import { refreshWorkspaceArtifacts } from '../workspaces/artifact-events.js';
|
|
@@ -24,6 +24,7 @@ import { InvalidWorkspacePathError, resolveWorkspaceFile, writeWorkspaceFile, }
|
|
|
24
24
|
import { GitWorkspaceError, commitWorkspaceGit, discardWorkspaceGit, pushWorkspaceGit, pullWorkspaceGit, abortWorkspaceMerge, saveWorkspaceGit, stageWorkspaceGit, unstageWorkspaceGit, } from '../workspaces/git.js';
|
|
25
25
|
import { ParameterEditError } from '../workspaces/json5-edit.js';
|
|
26
26
|
import { patchWorkspaceParameters, readWorkspaceParameters } from '../workspaces/parameters.js';
|
|
27
|
+
import { prepareParameterAgentTask, finishParameterAgentTask, verifyParameterAgentTask, } from '../workspaces/parameter-agent.js';
|
|
27
28
|
import { previewFailureCode, previewWorkspaceRoot, runPreviewPublish, trackPreviewPublish, } from '../previews/publish.js';
|
|
28
29
|
import { spawnBackgroundWriter, stopBackgroundWriter, } from '../processes/managed-writes.js';
|
|
29
30
|
import { wakeSourceEventUploader } from './source-uploader.js';
|
|
@@ -31,6 +32,11 @@ import { batchDirectoryHeld } from '../batch/occupancy.js';
|
|
|
31
32
|
import { BinaryWorkspaceWriteError, observeWorkspaceBinaryFile, writeWorkspaceBinaryFile } from '../workspaces/binary-write.js';
|
|
32
33
|
import { importWorkspaceLibraryAssets } from '../workspaces/asset-library-import.js';
|
|
33
34
|
import { createWorkspaceDirectory, WorkspaceDirectoryCreateError } from '../workspaces/directory-create.js';
|
|
35
|
+
import { DirectoryMutationError, mutateWorkspaceDirectory } from '../workspaces/directory-mutations.js';
|
|
36
|
+
import { observeWorkspaceDirectories } from '../workspaces/directory-revisions.js';
|
|
37
|
+
import { publishAssetTransfer } from '../workspaces/asset-transfer-publish.js';
|
|
38
|
+
import { AssetTransferError } from '../workspaces/asset-transfer-storage.js';
|
|
39
|
+
import { runDeliveryBuild, cancelDeliveryBuild, trackDeliveryBuild, DeliveryBuildError } from '../workspaces/delivery-build.js';
|
|
34
40
|
const CONVERSATION_KINDS = new Set([
|
|
35
41
|
'session.create',
|
|
36
42
|
'session.turn',
|
|
@@ -46,7 +52,10 @@ const LOCAL_KINDS = new Set([
|
|
|
46
52
|
'file.write',
|
|
47
53
|
'file.binary.write',
|
|
48
54
|
'asset.library.import',
|
|
55
|
+
'asset.bundle.save',
|
|
49
56
|
'directory.create',
|
|
57
|
+
'directory.rename',
|
|
58
|
+
'directory.delete',
|
|
50
59
|
'git.commit',
|
|
51
60
|
'git.push',
|
|
52
61
|
'git.pull',
|
|
@@ -58,9 +67,30 @@ const LOCAL_KINDS = new Set([
|
|
|
58
67
|
'workspace.references.import',
|
|
59
68
|
'terminal.create',
|
|
60
69
|
'parameter.patch',
|
|
70
|
+
'parameter.verify',
|
|
61
71
|
'preview.publish',
|
|
72
|
+
'delivery.build',
|
|
73
|
+
'delivery.cancel',
|
|
62
74
|
'skills.update',
|
|
63
75
|
]);
|
|
76
|
+
const parameterVerifications = new WeakMap();
|
|
77
|
+
const parameterVerificationClosing = new WeakSet();
|
|
78
|
+
export async function stopParameterVerifications(state) {
|
|
79
|
+
parameterVerificationClosing.add(state);
|
|
80
|
+
const tasks = parameterVerifications.get(state);
|
|
81
|
+
if (!tasks)
|
|
82
|
+
return;
|
|
83
|
+
for (const controller of tasks.keys())
|
|
84
|
+
controller.abort();
|
|
85
|
+
await Promise.allSettled(tasks.values());
|
|
86
|
+
}
|
|
87
|
+
export async function recoverInterruptedParameterVerifications(journal) {
|
|
88
|
+
for (const { scopeKey, operation } of journal.listOperations()) {
|
|
89
|
+
if (operation.kind !== 'parameter.verify' || !['queued', 'dispatched', 'accepted', 'uncertain'].includes(operation.status))
|
|
90
|
+
continue;
|
|
91
|
+
await journal.putOperation(scopeKey, transitionOperation(operation, 'interrupted', { error: 'parameter_verification_interrupted' }));
|
|
92
|
+
}
|
|
93
|
+
}
|
|
64
94
|
export function canExecuteKind(state, kind) {
|
|
65
95
|
if (LOCAL_KINDS.has(kind))
|
|
66
96
|
return true;
|
|
@@ -75,7 +105,7 @@ export async function executeOperation(state, scopeKey, operation, payload, reco
|
|
|
75
105
|
}
|
|
76
106
|
return executeScopedOperation(state, scopeKey, operation, payload, recovery);
|
|
77
107
|
};
|
|
78
|
-
if (['runtime.destroy', 'session.interrupt', 'session.approval.respond', 'session.steer', 'preview.publish'].includes(operation.kind)) {
|
|
108
|
+
if (['runtime.destroy', 'session.interrupt', 'session.approval.respond', 'session.steer', 'preview.publish', 'delivery.build', 'delivery.cancel'].includes(operation.kind)) {
|
|
79
109
|
return run();
|
|
80
110
|
}
|
|
81
111
|
return state.journal.exclusive(`workspace:${revisionScope(state, operation.workspaceId, operation.directoryName) ?? 'single'}`, run);
|
|
@@ -104,9 +134,15 @@ async function executeScopedOperation(state, scopeKey, operation, payload, recov
|
|
|
104
134
|
if (operation.kind === 'asset.library.import') {
|
|
105
135
|
return executeAssetLibraryImport(state, scopeKey, current, payload);
|
|
106
136
|
}
|
|
137
|
+
if (operation.kind === 'asset.bundle.save') {
|
|
138
|
+
return executeAssetBundleSave(state, scopeKey, current, payload);
|
|
139
|
+
}
|
|
107
140
|
if (operation.kind === 'directory.create') {
|
|
108
141
|
return executeDirectoryCreate(state, scopeKey, current, payload);
|
|
109
142
|
}
|
|
143
|
+
if (operation.kind === 'directory.rename' || operation.kind === 'directory.delete') {
|
|
144
|
+
return executeDirectoryMutation(state, scopeKey, current, payload);
|
|
145
|
+
}
|
|
110
146
|
if (operation.kind === 'workspace.references.import') {
|
|
111
147
|
return executeRemixReferenceImport(state, scopeKey, current, payload);
|
|
112
148
|
}
|
|
@@ -137,9 +173,18 @@ async function executeScopedOperation(state, scopeKey, operation, payload, recov
|
|
|
137
173
|
if (operation.kind === 'parameter.patch') {
|
|
138
174
|
return executeParameterPatch(state, scopeKey, current, payload);
|
|
139
175
|
}
|
|
176
|
+
if (operation.kind === 'parameter.verify') {
|
|
177
|
+
return executeParameterVerify(state, scopeKey, current, payload);
|
|
178
|
+
}
|
|
140
179
|
if (operation.kind === 'preview.publish') {
|
|
141
180
|
return executePreviewPublish(state, scopeKey, current);
|
|
142
181
|
}
|
|
182
|
+
if (operation.kind === 'delivery.build') {
|
|
183
|
+
return executeDeliveryBuild(state, scopeKey, current, payload);
|
|
184
|
+
}
|
|
185
|
+
if (operation.kind === 'delivery.cancel') {
|
|
186
|
+
return executeDeliveryCancel(state, scopeKey, current, payload);
|
|
187
|
+
}
|
|
143
188
|
if (!state.native?.ready()) {
|
|
144
189
|
return persist(state, scopeKey, transitionOperation(current, 'failed', { error: 'capability_unavailable' }));
|
|
145
190
|
}
|
|
@@ -312,6 +357,19 @@ async function executeTurn(state, scopeKey, operation, payload, recovery) {
|
|
|
312
357
|
if (!mode.success)
|
|
313
358
|
return persist(state, scopeKey, transitionOperation(operation, 'failed', { error: 'invalid_permission_mode' }));
|
|
314
359
|
const requestedMode = mode.data ?? found?.permissionMode ?? recovery?.permissionMode ?? 'auto';
|
|
360
|
+
const parameterTask = parameterAgentTaskRequestSchema.optional().safeParse(body.parameterTask);
|
|
361
|
+
if (!parameterTask.success || (parameterTask.data && parameterTask.data.taskId !== operation.clientRequestId))
|
|
362
|
+
return persist(state, scopeKey, transitionOperation(operation, 'failed', { error: 'invalid_parameter_task' }));
|
|
363
|
+
if (parameterTask.data) {
|
|
364
|
+
if (operation.directoryName === 'root')
|
|
365
|
+
return persist(state, scopeKey, transitionOperation(operation, 'failed', { error: 'invalid_workspace_directory' }));
|
|
366
|
+
if ((selection.data.workMode ?? 'execute') !== 'execute' || requestedMode === 'read-only')
|
|
367
|
+
return persist(state, scopeKey, transitionOperation(operation, 'failed', { error: 'parameter_task_readonly' }));
|
|
368
|
+
if (workspaceSessions(state, operation.workspaceId, operation.directoryName).some(sessionTurnBusy))
|
|
369
|
+
return persist(state, scopeKey, transitionOperation(operation, 'failed', { error: 'turn_already_active' }));
|
|
370
|
+
if (await batchDirectoryHeld(state, operation.workspaceId, operation.directoryName))
|
|
371
|
+
return persist(state, scopeKey, transitionOperation(operation, 'failed', { error: 'batch_workspace_readonly' }));
|
|
372
|
+
}
|
|
315
373
|
const modelChanged = selected.data !== undefined && selected.data !== (found?.modelConfigId ?? recovery?.modelConfigId);
|
|
316
374
|
const requestedEffort = effort.data ?? (modelChanged ? undefined : recovery?.reasoningEffort ?? found?.reasoningEffort);
|
|
317
375
|
if (recovery && found?.recoverability !== 'native') {
|
|
@@ -383,49 +441,72 @@ async function executeTurn(state, scopeKey, operation, payload, recovery) {
|
|
|
383
441
|
return persist(state, scopeKey, transitionOperation(operation, 'failed', { error: 'cli_credentials_unavailable' }));
|
|
384
442
|
}
|
|
385
443
|
}
|
|
444
|
+
let parameterInstructions = '';
|
|
445
|
+
if (parameterTask.data) {
|
|
446
|
+
try {
|
|
447
|
+
parameterInstructions = (await prepareParameterAgentTask(state, operation, parameterTask.data, root)).instructions;
|
|
448
|
+
}
|
|
449
|
+
catch (error) {
|
|
450
|
+
return persist(state, scopeKey, transitionOperation(operation, 'failed', { error: parameterAgentError(error) }));
|
|
451
|
+
}
|
|
452
|
+
}
|
|
386
453
|
// Taken before the native turn exists, so nothing the agent writes can precede it.
|
|
387
454
|
await beginTurnChanges(state, sessionId, {
|
|
388
455
|
root, workspaceId: operation.workspaceId, directoryName: binding.directoryName ?? 'basic',
|
|
389
456
|
});
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
457
|
+
let result;
|
|
458
|
+
try {
|
|
459
|
+
result = await native(state).startTurn({
|
|
460
|
+
sessionId,
|
|
461
|
+
threadId: binding.threadId,
|
|
462
|
+
text: composer.append(batchContext + context + String(body.text ?? '') + attachmentText, [engineeringBatchContext, parameterInstructions].filter(Boolean).join('\n\n')),
|
|
463
|
+
images: attachments.filter((file) => ['image/png', 'image/jpeg', 'image/gif', 'image/webp'].includes(file.mimeType)).map((file) => file.path),
|
|
464
|
+
clientRequestId: operation.clientRequestId,
|
|
465
|
+
modelConfigId: selected.data ?? binding.modelConfigId,
|
|
466
|
+
reasoningEffort: requestedEffort,
|
|
467
|
+
permissionMode: requestedMode,
|
|
468
|
+
workMode: selection.data.workMode ?? 'execute',
|
|
469
|
+
skillsLockHash: composer.skillsLockHash,
|
|
470
|
+
uiSnapshot: uiSnapshot.data,
|
|
471
|
+
simulateLostReceipt: body.simulateLostReceipt === true,
|
|
472
|
+
persistReservation: async ({ nativeRequestId, modelConfigId, reasoningEffort, permissionMode }) => {
|
|
473
|
+
const selectedEffort = reasoningEffort ?? requestedEffort;
|
|
474
|
+
const selectedMode = permissionMode ?? requestedMode;
|
|
475
|
+
await persist(state, scopeKey, {
|
|
476
|
+
...operation,
|
|
477
|
+
nativeRequestId,
|
|
478
|
+
nativeThreadId: binding.threadId,
|
|
479
|
+
modelConfigId,
|
|
480
|
+
reasoningEffort: selectedEffort,
|
|
481
|
+
permissionMode: selectedMode,
|
|
482
|
+
workMode: selection.data.workMode ?? 'execute',
|
|
483
|
+
context: composer.context,
|
|
484
|
+
userText: selection.data.workMode !== undefined || composer.context !== undefined || parameterTask.data
|
|
485
|
+
? String(body.userText ?? body.text ?? '') : undefined,
|
|
486
|
+
updatedAt: new Date().toISOString(),
|
|
487
|
+
});
|
|
488
|
+
await state.journal.putSession({ ...binding, modelConfigId, reasoningEffort: selectedEffort, permissionMode: selectedMode, turnStatus: 'starting' });
|
|
489
|
+
},
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
catch (error) {
|
|
493
|
+
if (!parameterTask.data)
|
|
494
|
+
throw error;
|
|
495
|
+
abandonTurnChanges(state, sessionId);
|
|
496
|
+
if (error instanceof NativePreparationError) {
|
|
497
|
+
await state.journal.putSession({ ...(state.journal.getSession(sessionId) ?? binding), turnStatus: undefined, turnBlock: undefined });
|
|
498
|
+
return persist(state, scopeKey, transitionOperation(state.journal.getOperation(scopeKey) ?? operation, 'failed', { error: error.message }));
|
|
499
|
+
}
|
|
500
|
+
await state.journal.putSession({ ...(state.journal.getSession(sessionId) ?? binding), turnStatus: 'starting', turnBlock: 'uncertain' });
|
|
501
|
+
return persist(state, scopeKey, transitionOperation(state.journal.getOperation(scopeKey) ?? operation, 'uncertain', { error: 'native_receipt_lost' }));
|
|
502
|
+
}
|
|
422
503
|
const reserved = state.journal.getOperation(scopeKey) ?? operation;
|
|
423
504
|
if (result.outcome === 'uncertain' || result.outcome === 'failed' || !result.turnId) {
|
|
424
505
|
abandonTurnChanges(state, sessionId);
|
|
425
506
|
const failed = result.outcome === 'failed';
|
|
426
507
|
// Codex no longer knows the thread although the binding said it could continue. Marking the
|
|
427
508
|
// binding lost lets the same turn, sent again with the conversation, rebind the session.
|
|
428
|
-
const threadLost = failed && result.error === 'unknown_thread';
|
|
509
|
+
const threadLost = result.outcome === 'failed' && result.error === 'unknown_thread';
|
|
429
510
|
await state.journal.putSession({
|
|
430
511
|
...(state.journal.getSession(sessionId) ?? binding),
|
|
431
512
|
turnStatus: failed ? undefined : 'starting',
|
|
@@ -471,7 +552,7 @@ async function executeTurn(state, scopeKey, operation, payload, recovery) {
|
|
|
471
552
|
threadId: binding.threadId,
|
|
472
553
|
turnId: result.turnId,
|
|
473
554
|
});
|
|
474
|
-
if (selection.data.workMode !== undefined || composer.context !== undefined) {
|
|
555
|
+
if (selection.data.workMode !== undefined || composer.context !== undefined || parameterTask.data) {
|
|
475
556
|
await recordNativeEvents(state, accepted, [{
|
|
476
557
|
type: 'composer.context', occurredAt: new Date().toISOString(), payload: {
|
|
477
558
|
threadId: binding.threadId, turnId: result.turnId, clientRequestId: operation.clientRequestId,
|
|
@@ -488,7 +569,56 @@ async function executeTurn(state, scopeKey, operation, payload, recovery) {
|
|
|
488
569
|
const session = state.journal.getSession(sessionId) ?? binding;
|
|
489
570
|
await state.journal.putSession({ ...session, turnStatus: 'waiting_for_user', activeTurnId: result.turnId });
|
|
490
571
|
}
|
|
491
|
-
|
|
572
|
+
const final = state.journal.getOperation(scopeKey) ?? latest;
|
|
573
|
+
await syncParameterAgentOperation(state, final);
|
|
574
|
+
return final;
|
|
575
|
+
}
|
|
576
|
+
async function executeParameterVerify(state, scopeKey, operation, payload) {
|
|
577
|
+
const parsed = parameterAgentVerifyPayloadSchema.safeParse(payload);
|
|
578
|
+
if (!parsed.success)
|
|
579
|
+
return persist(state, scopeKey, transitionOperation(operation, 'failed', { error: 'invalid_parameter_task' }));
|
|
580
|
+
if (operation.directoryName === 'root' || (operation.sessionId && productionSessionAttemptId(operation.sessionId)))
|
|
581
|
+
return persist(state, scopeKey, transitionOperation(operation, 'failed', { error: 'invalid_workspace_directory' }));
|
|
582
|
+
if (workspaceSessions(state, operation.workspaceId, operation.directoryName).some(sessionTurnBusy))
|
|
583
|
+
return persist(state, scopeKey, transitionOperation(operation, 'failed', { error: 'turn_already_active' }));
|
|
584
|
+
if (await batchDirectoryHeld(state, operation.workspaceId, operation.directoryName))
|
|
585
|
+
return persist(state, scopeKey, transitionOperation(operation, 'failed', { error: 'batch_workspace_readonly' }));
|
|
586
|
+
const root = await workspaceRoot(state, operation.workspaceId, operation.directoryName);
|
|
587
|
+
const accepted = await persist(state, scopeKey, transitionOperation(operation, 'accepted'));
|
|
588
|
+
const controller = new AbortController();
|
|
589
|
+
const assertActive = () => {
|
|
590
|
+
if (controller.signal.aborted || parameterVerificationClosing.has(state))
|
|
591
|
+
throw new Error('parameter_verification_interrupted');
|
|
592
|
+
if (state.journal.destroyed())
|
|
593
|
+
throw new Error('runtime_destroyed');
|
|
594
|
+
if ((operation.leaseFence ?? 0) < state.acceptedLeaseFence)
|
|
595
|
+
throw new Error('stale_lease');
|
|
596
|
+
};
|
|
597
|
+
const key = `workspace:${revisionScope(state, operation.workspaceId, operation.directoryName) ?? 'single'}`;
|
|
598
|
+
const task = state.journal.exclusive(key, async () => {
|
|
599
|
+
try {
|
|
600
|
+
assertActive();
|
|
601
|
+
if (workspaceSessions(state, operation.workspaceId, operation.directoryName).some(sessionTurnBusy))
|
|
602
|
+
throw new Error('turn_already_active');
|
|
603
|
+
if (await batchDirectoryHeld(state, operation.workspaceId, operation.directoryName))
|
|
604
|
+
throw new Error('batch_workspace_readonly');
|
|
605
|
+
const result = await verifyParameterAgentTask(state, operation, parsed.data, root, { assertActive, signal: controller.signal });
|
|
606
|
+
assertActive();
|
|
607
|
+
await persist(state, scopeKey, transitionOperation(accepted, 'completed', { result }));
|
|
608
|
+
}
|
|
609
|
+
catch (error) {
|
|
610
|
+
await persist(state, scopeKey, transitionOperation(accepted, 'failed', { error: parameterAgentError(error) }));
|
|
611
|
+
}
|
|
612
|
+
});
|
|
613
|
+
const tasks = parameterVerifications.get(state) ?? new Map();
|
|
614
|
+
parameterVerifications.set(state, tasks);
|
|
615
|
+
const tracked = task.catch(() => undefined).finally(() => tasks.delete(controller));
|
|
616
|
+
tasks.set(controller, tracked);
|
|
617
|
+
return accepted;
|
|
618
|
+
}
|
|
619
|
+
function parameterAgentError(error) {
|
|
620
|
+
return error instanceof Error && /^(?:parameter_|invalid_parameter_|runtime_destroyed$|stale_lease$|turn_already_active$|batch_workspace_readonly$)/.test(error.message)
|
|
621
|
+
? error.message : 'parameter_task_failed';
|
|
492
622
|
}
|
|
493
623
|
/**
|
|
494
624
|
* Gives a session whose native thread was lost a thread in this process. It keeps its own thread
|
|
@@ -733,6 +863,7 @@ async function executeApproval(state, scopeKey, operation, payload) {
|
|
|
733
863
|
}
|
|
734
864
|
async function executeDestroy(state, scopeKey, operation) {
|
|
735
865
|
await state.journal.markDestroyed();
|
|
866
|
+
await stopParameterVerifications(state);
|
|
736
867
|
await closeCliCredentials(state);
|
|
737
868
|
await state.gitCredentials?.destroy();
|
|
738
869
|
disposeTerminalHost(state);
|
|
@@ -783,6 +914,50 @@ async function executeDirectoryCreate(state, scopeKey, operation, payload) {
|
|
|
783
914
|
return persist(state, scopeKey, transitionOperation(operation, 'failed', { error: code }));
|
|
784
915
|
}
|
|
785
916
|
}
|
|
917
|
+
async function executeDirectoryMutation(state, scopeKey, operation, payload) {
|
|
918
|
+
const kind = operation.kind === 'directory.rename' ? 'directory.rename' : 'directory.delete';
|
|
919
|
+
const parsed = kind === 'directory.rename' ? directoryRenamePayloadSchema.safeParse(payload) : directoryDeletePayloadSchema.safeParse(payload);
|
|
920
|
+
if (!parsed.success)
|
|
921
|
+
return persist(state, scopeKey, transitionOperation(operation, 'failed', { error: 'invalid_workspace_path' }));
|
|
922
|
+
let current = operation;
|
|
923
|
+
try {
|
|
924
|
+
const assertCurrent = async () => {
|
|
925
|
+
if ((operation.leaseFence ?? 0) < state.acceptedLeaseFence || state.journal.destroyed())
|
|
926
|
+
throw new DirectoryMutationError('stale_lease');
|
|
927
|
+
if (operation.sessionId && productionSessionAttemptId(operation.sessionId))
|
|
928
|
+
throw new DirectoryMutationError('production_session_readonly');
|
|
929
|
+
if (await batchDirectoryHeld(state, operation.workspaceId, operation.directoryName))
|
|
930
|
+
throw new DirectoryMutationError('batch_workspace_readonly');
|
|
931
|
+
};
|
|
932
|
+
await assertCurrent();
|
|
933
|
+
const root = await workspaceRoot(state, operation.workspaceId, operation.directoryName);
|
|
934
|
+
const scope = revisionScope(state, operation.workspaceId, operation.directoryName);
|
|
935
|
+
const options = { operationId: operation.operationId, operationPayloadHash: operation.payloadHash, scope: scopeKey, assertCurrent,
|
|
936
|
+
observeRevision: async () => (await observeWorkspaceDirectories(root, state.journal, scope)).directoryRevisions?.[parsed.data.path],
|
|
937
|
+
onDetached: async () => { if (current.status === 'dispatched')
|
|
938
|
+
current = await persist(state, scopeKey, transitionOperation(current, 'accepted')); } };
|
|
939
|
+
const receipt = await mutateWorkspaceDirectory(root, kind, parsed.data, options);
|
|
940
|
+
if (!receipt)
|
|
941
|
+
throw new DirectoryMutationError('directory_recovery_required');
|
|
942
|
+
await observeWorkspaceDirectories(root, state.journal, scope);
|
|
943
|
+
const paths = [receipt.path, ...(receipt.newPath ? [receipt.newPath] : [])];
|
|
944
|
+
const result = { paths, skippedPaths: [], failures: [], directoryMutation: directoryMutationReceiptSchema.parse(receipt) };
|
|
945
|
+
if (current.status === 'dispatched')
|
|
946
|
+
current = await persist(state, scopeKey, transitionOperation(current, 'accepted', { result }));
|
|
947
|
+
await recordNativeEvents(state, current, [{ type: 'files.changed', occurredAt: new Date().toISOString(), payload: {
|
|
948
|
+
workspaceId: operation.workspaceId, directoryName: operation.directoryName ?? 'basic', paths,
|
|
949
|
+
source: kind, operationId: operation.operationId, revision: receipt.revision,
|
|
950
|
+
} }]);
|
|
951
|
+
return persist(state, scopeKey, transitionOperation(current, 'completed', { result }));
|
|
952
|
+
}
|
|
953
|
+
catch (error) {
|
|
954
|
+
const code = error instanceof DirectoryMutationError ? error.code
|
|
955
|
+
: error instanceof InvalidWorkspacePathError || error?.code === 'invalid_workspace_path' ? 'invalid_workspace_path'
|
|
956
|
+
: ['EACCES', 'EPERM'].includes(String(error?.code)) ? 'workspace_permission_denied'
|
|
957
|
+
: 'directory_recovery_required';
|
|
958
|
+
return persist(state, scopeKey, transitionOperation(current, code === 'directory_recovery_required' ? 'uncertain' : 'failed', { error: code }));
|
|
959
|
+
}
|
|
960
|
+
}
|
|
786
961
|
async function executeAssetLibraryImport(state, scopeKey, operation, payload) {
|
|
787
962
|
const input = payload;
|
|
788
963
|
try {
|
|
@@ -824,6 +999,63 @@ async function executeAssetLibraryImport(state, scopeKey, operation, payload) {
|
|
|
824
999
|
}));
|
|
825
1000
|
}
|
|
826
1001
|
}
|
|
1002
|
+
async function executeAssetBundleSave(state, scopeKey, operation, payload) {
|
|
1003
|
+
const directoryName = operation.directoryName ?? 'basic';
|
|
1004
|
+
const scope = revisionScope(state, operation.workspaceId, directoryName);
|
|
1005
|
+
const revisions = new Map();
|
|
1006
|
+
const metadata = new Map();
|
|
1007
|
+
try {
|
|
1008
|
+
const assertCurrent = async () => {
|
|
1009
|
+
if (state.journal.destroyed() || (operation.leaseFence ?? 0) < state.acceptedLeaseFence)
|
|
1010
|
+
throw new AssetTransferError(state.journal.destroyed() ? 'runtime_destroyed' : 'stale_lease');
|
|
1011
|
+
if (directoryName === 'root' || operation.sessionId && productionSessionAttemptId(operation.sessionId))
|
|
1012
|
+
throw new AssetTransferError('production_session_readonly');
|
|
1013
|
+
if (await batchDirectoryHeld(state, operation.workspaceId, directoryName))
|
|
1014
|
+
throw new AssetTransferError('batch_workspace_readonly');
|
|
1015
|
+
};
|
|
1016
|
+
await assertCurrent();
|
|
1017
|
+
const root = await workspaceRoot(state, operation.workspaceId, directoryName);
|
|
1018
|
+
const receipt = await publishAssetTransfer(root, { environment: state.config.environment, userId: state.config.userId,
|
|
1019
|
+
workspaceId: operation.workspaceId ?? '', directoryName }, payload, { operationId: operation.operationId, assertCurrent,
|
|
1020
|
+
onManifest: manifest => { for (const file of manifest.files)
|
|
1021
|
+
metadata.set(file.path, file); },
|
|
1022
|
+
assertRevision: async (file, observed) => {
|
|
1023
|
+
const previous = state.journal.fileRevisions(scope)[file.path];
|
|
1024
|
+
const hash = observed?.contentHash ?? '';
|
|
1025
|
+
const revision = previous ? previous.revision + (previous.contentHash === hash ? 0 : 1) : 0;
|
|
1026
|
+
if (previous && previous.contentHash !== hash || !previous && observed)
|
|
1027
|
+
await state.journal.putFileRevision(file.path, { revision, contentHash: hash }, scope);
|
|
1028
|
+
if (file.expectedRevision !== undefined && file.expectedRevision !== revision)
|
|
1029
|
+
throw new AssetTransferError('revision_conflict');
|
|
1030
|
+
revisions.set(file.path, revision + 1);
|
|
1031
|
+
} });
|
|
1032
|
+
await assertCurrent();
|
|
1033
|
+
const paths = [...receipt.files.map(file => file.path), ...(receipt.directories ?? [])];
|
|
1034
|
+
const result = { paths, skippedPaths: [], failures: [], bundleReceipt: receipt };
|
|
1035
|
+
const events = [];
|
|
1036
|
+
for (const file of receipt.files) {
|
|
1037
|
+
const previous = state.journal.fileRevisions(scope)[file.path];
|
|
1038
|
+
const revision = revisions.get(file.path) ?? (previous ? previous.revision + (previous.contentHash === file.contentHash ? 0 : 1) : 1);
|
|
1039
|
+
await state.journal.putFileRevision(file.path, { revision, contentHash: file.contentHash }, scope);
|
|
1040
|
+
events.push({ type: 'file.written', payload: { ...file, revision, writer: 'manual', workspaceId: operation.workspaceId,
|
|
1041
|
+
directoryName, operationId: operation.operationId, transferId: receipt.transferId,
|
|
1042
|
+
...(metadata.get(file.path)?.processing ? { processing: metadata.get(file.path).processing } : {}) }, occurredAt: new Date().toISOString() });
|
|
1043
|
+
}
|
|
1044
|
+
events.push({ type: 'files.changed', payload: { workspaceId: operation.workspaceId, directoryName,
|
|
1045
|
+
operationId: operation.operationId, paths, source: 'asset.bundle.save' }, occurredAt: new Date().toISOString() });
|
|
1046
|
+
const accepted = await persist(state, scopeKey, transitionOperation(operation, 'accepted', { result }));
|
|
1047
|
+
await recordNativeEvents(state, accepted, events);
|
|
1048
|
+
if (operation.workspaceId && directoryName === 'basic')
|
|
1049
|
+
await refreshWorkspaceArtifacts(state, operation.workspaceId, { paths, source: { kind: 'discovered', tool: 'asset.bundle.save', operationId: operation.operationId } }).catch(() => undefined);
|
|
1050
|
+
return persist(state, scopeKey, transitionOperation(accepted, 'completed', { result }));
|
|
1051
|
+
}
|
|
1052
|
+
catch (error) {
|
|
1053
|
+
const code = error instanceof AssetTransferError ? error.code
|
|
1054
|
+
: error.name === 'ZodError' ? 'invalid_asset_transfer'
|
|
1055
|
+
: ['EACCES', 'EPERM'].includes(String(error.code)) ? 'workspace_permission_denied' : 'asset_bundle_save_failed';
|
|
1056
|
+
return persist(state, scopeKey, transitionOperation(operation, 'failed', { error: code }));
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
827
1059
|
async function executeFileBinaryWrite(state, scopeKey, operation, payload) {
|
|
828
1060
|
const parsed = fileBinaryWritePayloadSchema.safeParse(payload);
|
|
829
1061
|
if (!parsed.success)
|
|
@@ -1109,19 +1341,19 @@ async function executeParameterPatch(state, scopeKey, operation, payload) {
|
|
|
1109
1341
|
const root = await workspaceRoot(state, operation.workspaceId, operation.directoryName);
|
|
1110
1342
|
const scope = revisionScope(state, operation.workspaceId, operation.directoryName);
|
|
1111
1343
|
const document = await readWorkspaceParameters(root, state.journal.fileRevisions(scope));
|
|
1112
|
-
if (document.status === 'missing' || document.status === 'schema_only' || !document.dataPath) {
|
|
1113
|
-
return persist(state, scopeKey, transitionOperation(operation, 'failed', { error: 'parameter_data_missing' }));
|
|
1114
|
-
}
|
|
1115
1344
|
if (document.status === 'invalid') {
|
|
1116
1345
|
return persist(state, scopeKey, transitionOperation(operation, 'failed', { error: 'parameter_document_invalid' }));
|
|
1117
1346
|
}
|
|
1347
|
+
if (document.status === 'missing' || document.status === 'schema_only' || !document.dataPath) {
|
|
1348
|
+
return persist(state, scopeKey, transitionOperation(operation, 'failed', { error: 'parameter_data_missing' }));
|
|
1349
|
+
}
|
|
1118
1350
|
const dataPath = document.dataPath;
|
|
1119
|
-
return withObservedFile(state, operation.workspaceId, dataPath, async ({ revision: currentRevision }) => {
|
|
1351
|
+
return withObservedFile(state, operation.workspaceId, dataPath, async ({ revision: currentRevision, file }) => {
|
|
1120
1352
|
if (parsed.data.expectedRevision !== currentRevision) {
|
|
1121
1353
|
return persist(state, scopeKey, transitionOperation(operation, 'failed', { error: 'revision_conflict' }));
|
|
1122
1354
|
}
|
|
1123
1355
|
try {
|
|
1124
|
-
const written = await patchWorkspaceParameters(root, state.journal.fileRevisions(scope), parsed.data);
|
|
1356
|
+
const written = await patchWorkspaceParameters(root, state.journal.fileRevisions(scope), parsed.data, { dataPath, contentHash: file?.contentHash ?? '' });
|
|
1125
1357
|
const revision = currentRevision + 1;
|
|
1126
1358
|
await state.journal.putFileRevision(dataPath, { revision, contentHash: written.contentHash }, scope);
|
|
1127
1359
|
const accepted = await persist(state, scopeKey, transitionOperation(operation, 'accepted', { expectedRevision: revision }));
|
|
@@ -1133,7 +1365,9 @@ async function executeParameterPatch(state, scopeKey, operation, payload) {
|
|
|
1133
1365
|
revision,
|
|
1134
1366
|
contentHash: written.contentHash,
|
|
1135
1367
|
size: written.size,
|
|
1136
|
-
|
|
1368
|
+
...('changes' in parsed.data
|
|
1369
|
+
? { fieldPaths: parsed.data.changes.map((change) => change.fieldPath) }
|
|
1370
|
+
: { fieldPath: parsed.data.fieldPath }),
|
|
1137
1371
|
},
|
|
1138
1372
|
occurredAt: new Date().toISOString(),
|
|
1139
1373
|
},
|
|
@@ -1165,6 +1399,42 @@ async function executePreviewPublish(state, scopeKey, operation) {
|
|
|
1165
1399
|
}).catch(() => undefined));
|
|
1166
1400
|
return accepted;
|
|
1167
1401
|
}
|
|
1402
|
+
async function executeDeliveryBuild(state, scopeKey, operation, payload) {
|
|
1403
|
+
const parsed = deliveryBuildPayloadSchema.safeParse(payload);
|
|
1404
|
+
if (!parsed.success) {
|
|
1405
|
+
return persist(state, scopeKey, transitionOperation(operation, 'failed', { error: 'invalid_delivery_build' }));
|
|
1406
|
+
}
|
|
1407
|
+
if (!operation.workspaceId) {
|
|
1408
|
+
return persist(state, scopeKey, transitionOperation(operation, 'failed', { error: 'invalid_workspace' }));
|
|
1409
|
+
}
|
|
1410
|
+
const accepted = await persist(state, scopeKey, transitionOperation(operation, 'accepted'));
|
|
1411
|
+
const task = runDeliveryBuild(state, {
|
|
1412
|
+
buildId: operation.operationId,
|
|
1413
|
+
workspaceId: operation.workspaceId,
|
|
1414
|
+
directoryName: operation.directoryName ?? 'basic',
|
|
1415
|
+
payload: parsed.data,
|
|
1416
|
+
actor: parsed.data.actor ?? 'user',
|
|
1417
|
+
}).then(async (result) => {
|
|
1418
|
+
const error = result.record.error
|
|
1419
|
+
?? (result.record.status === 'succeeded' ? undefined : 'delivery_build_failed');
|
|
1420
|
+
await persist(state, scopeKey, transitionOperation(accepted, result.record.status === 'succeeded' ? 'completed' : 'failed', error ? { error } : {}));
|
|
1421
|
+
}, async (error) => {
|
|
1422
|
+
await persist(state, scopeKey, transitionOperation(accepted, 'failed', {
|
|
1423
|
+
error: error instanceof DeliveryBuildError ? error.code : 'delivery_build_failed',
|
|
1424
|
+
}));
|
|
1425
|
+
}).catch(() => undefined);
|
|
1426
|
+
trackDeliveryBuild(state, task);
|
|
1427
|
+
return accepted;
|
|
1428
|
+
}
|
|
1429
|
+
async function executeDeliveryCancel(state, scopeKey, operation, payload) {
|
|
1430
|
+
const parsed = deliveryCancelPayloadSchema.safeParse(payload);
|
|
1431
|
+
if (!parsed.success || !operation.workspaceId) {
|
|
1432
|
+
return persist(state, scopeKey, transitionOperation(operation, 'failed', { error: 'invalid_delivery_cancel' }));
|
|
1433
|
+
}
|
|
1434
|
+
const accepted = await persist(state, scopeKey, transitionOperation(operation, 'accepted'));
|
|
1435
|
+
const canceled = await cancelDeliveryBuild(state, operation.workspaceId, operation.directoryName ?? 'basic', parsed.data.buildId);
|
|
1436
|
+
return persist(state, scopeKey, transitionOperation(accepted, canceled ? 'completed' : 'failed', canceled ? {} : { error: 'delivery_build_not_found' }));
|
|
1437
|
+
}
|
|
1168
1438
|
async function executeTerminalCreate(state, scopeKey, operation, payload) {
|
|
1169
1439
|
const parsed = terminalCreatePayloadSchema.safeParse(payload ?? {});
|
|
1170
1440
|
if (!parsed.success) {
|
|
@@ -1280,6 +1550,11 @@ export async function ingestCollected(state, event, publish = true) {
|
|
|
1280
1550
|
itemId: ids.itemId,
|
|
1281
1551
|
occurredAt: event.occurredAt,
|
|
1282
1552
|
});
|
|
1553
|
+
if (ids.turnId && ids.threadId) {
|
|
1554
|
+
const operation = state.journal.listOperations().map(item => item.operation).find(item => item.kind === 'session.turn' && item.nativeThreadId === ids.threadId && item.nativeTurnId === ids.turnId);
|
|
1555
|
+
if (operation)
|
|
1556
|
+
await syncParameterAgentOperation(state, operation);
|
|
1557
|
+
}
|
|
1283
1558
|
if (publish && recorded.length > 0)
|
|
1284
1559
|
await publishSourceEvents(state, recorded);
|
|
1285
1560
|
return recorded;
|
|
@@ -1329,7 +1604,16 @@ function nativeIds(params) {
|
|
|
1329
1604
|
};
|
|
1330
1605
|
}
|
|
1331
1606
|
async function persist(state, scopeKey, operation) {
|
|
1332
|
-
|
|
1607
|
+
const saved = await state.journal.putOperation(scopeKey, operation);
|
|
1608
|
+
await syncParameterAgentOperation(state, saved);
|
|
1609
|
+
return saved;
|
|
1610
|
+
}
|
|
1611
|
+
async function syncParameterAgentOperation(state, operation) {
|
|
1612
|
+
if (operation.kind !== 'session.turn')
|
|
1613
|
+
return;
|
|
1614
|
+
if (operation.status === 'completed' || operation.status === 'failed' || operation.status === 'interrupted' || operation.status === 'uncertain') {
|
|
1615
|
+
await finishParameterAgentTask(state, operation, operation.status, operation.error);
|
|
1616
|
+
}
|
|
1333
1617
|
}
|
|
1334
1618
|
function native(state) {
|
|
1335
1619
|
if (!state.native)
|