@playcraft/cli 0.1.66 → 0.1.68
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/assets/client.js +4 -2
- package/dist/assets/messages.js +2 -0
- package/dist/audio-source/messages.js +35 -0
- package/dist/commands/assets.js +1 -1
- package/dist/commands/audio-program.js +82 -0
- package/dist/commands/audio-source.js +109 -0
- package/dist/commands/audio.js +13 -4
- package/dist/commands/build.js +4 -2
- package/dist/commands/tools-context.js +46 -0
- package/dist/commands/tools.js +2 -0
- package/dist/project-skills/builtin-import.js +69 -22
- package/dist/project-skills/commands.js +11 -2
- package/dist/project-skills/lifecycle.js +1 -1
- package/dist/project-skills/local-store.js +90 -31
- package/dist/project-skills/messages.js +16 -0
- package/dist/project-skills/migrate-lock.js +166 -0
- package/dist/project-skills/remote-cache.js +36 -5
- package/dist/utils/agent-api-client.js +18 -1
- package/dist/utils/agent-generation-provenance.js +8 -2
- package/dist/utils/audio-export.js +249 -0
- package/dist/utils/audio-processor.js +22 -99
- package/dist/utils/execution-context-bridge.js +28 -0
- package/dist/workspace-runtime/batch/control.js +13 -2
- package/dist/workspace-runtime/batch/generate.js +43 -2
- package/dist/workspace-runtime/batch/prepare.js +4 -0
- package/dist/workspace-runtime/batch/skill-artifacts.js +103 -0
- package/dist/workspace-runtime/batch/source-capture.js +6 -8
- package/dist/workspace-runtime/codex/app-server.js +26 -2
- package/dist/workspace-runtime/codex/execution-context-broker.js +61 -0
- package/dist/workspace-runtime/codex/execution-context-tools.js +37 -0
- package/dist/workspace-runtime/codex/generation-provenance-broker.js +9 -6
- package/dist/workspace-runtime/codex/level-editor-tools.js +18 -0
- package/dist/workspace-runtime/codex/model-broker.js +15 -2
- package/dist/workspace-runtime/codex/scheduled-task-tools.js +1 -1
- package/dist/workspace-runtime/previews/finalize.js +1 -1
- package/dist/workspace-runtime/previews/output.js +4 -1
- package/dist/workspace-runtime/previews/publish.js +5 -2
- package/dist/workspace-runtime/previews/runtime-preview.js +38 -3
- package/dist/workspace-runtime/serve.js +6 -0
- package/dist/workspace-runtime/server/accept-operation.js +85 -13
- package/dist/workspace-runtime/server/audio-assets.js +257 -0
- package/dist/workspace-runtime/server/audio-export.js +136 -0
- package/dist/workspace-runtime/server/directory-access.js +222 -67
- package/dist/workspace-runtime/server/dispatch.js +252 -29
- package/dist/workspace-runtime/server/http.js +97 -3
- package/dist/workspace-runtime/server/operation-record.js +1 -1
- package/dist/workspace-runtime/workspaces/artifact-files.js +10 -3
- package/dist/workspace-runtime/workspaces/artifacts.js +47 -6
- package/dist/workspace-runtime/workspaces/asset-library-import.js +27 -7
- package/dist/workspace-runtime/workspaces/audio-assets/artifact-package.js +104 -0
- package/dist/workspace-runtime/workspaces/audio-assets/binding-usage.js +71 -0
- package/dist/workspace-runtime/workspaces/audio-assets/build-inputs.js +80 -0
- package/dist/workspace-runtime/workspaces/audio-assets/library-import.js +149 -0
- package/dist/workspace-runtime/workspaces/audio-assets/library-package.js +26 -0
- package/dist/workspace-runtime/workspaces/audio-assets/program-artifact-audit.js +90 -0
- package/dist/workspace-runtime/workspaces/audio-assets/program-browser.js +194 -0
- package/dist/workspace-runtime/workspaces/audio-assets/program-build-inputs.js +122 -0
- package/dist/workspace-runtime/workspaces/audio-assets/program-engine.js +39 -0
- package/dist/workspace-runtime/workspaces/audio-assets/program-package.js +289 -0
- package/dist/workspace-runtime/workspaces/audio-assets/program-variant-build.js +217 -0
- package/dist/workspace-runtime/workspaces/audio-assets/references.js +107 -0
- package/dist/workspace-runtime/workspaces/audio-assets/render-worker.js +117 -0
- package/dist/workspace-runtime/workspaces/audio-assets/renderer.js +128 -0
- package/dist/workspace-runtime/workspaces/audio-assets/representations.js +73 -0
- package/dist/workspace-runtime/workspaces/audio-assets/service.js +459 -0
- package/dist/workspace-runtime/workspaces/audio-assets/storage.js +125 -0
- package/dist/workspace-runtime/workspaces/build-isolation.js +2 -0
- package/dist/workspace-runtime/workspaces/composer-context.js +40 -1
- package/dist/workspace-runtime/workspaces/empty-template.js +1 -1
- package/dist/workspace-runtime/workspaces/execution-context.js +24 -0
- package/dist/workspace-runtime/workspaces/files.js +3 -5
- package/dist/workspace-runtime/workspaces/git.js +7 -0
- package/dist/workspace-runtime/workspaces/level-editor-agent-store.js +143 -0
- package/dist/workspace-runtime/workspaces/level-editor-agent.js +410 -0
- package/dist/workspace-runtime/workspaces/level-editor-atomic.js +424 -0
- package/dist/workspace-runtime/workspaces/level-editor-extensions.js +140 -0
- package/dist/workspace-runtime/workspaces/level-editor-format.js +75 -0
- package/dist/workspace-runtime/workspaces/level-editor-history-store.js +250 -0
- package/dist/workspace-runtime/workspaces/level-editor-json5.js +132 -0
- package/dist/workspace-runtime/workspaces/level-editor-requests.js +724 -0
- package/dist/workspace-runtime/workspaces/level-editor.js +454 -0
- package/dist/workspace-runtime/workspaces/parameter-initialization-skill.js +62 -22
- package/dist/workspace-runtime/workspaces/parameter-production.js +1 -1
- package/dist/workspace-runtime/workspaces/remix-reference-skill.js +4 -3
- package/dist/workspace-runtime/workspaces/skill-artifact-loader.js +19 -5
- package/dist/workspace-runtime/workspaces/skills.js +48 -13
- package/package.json +9 -4
- package/project-template/.claude/skills/playcraft-audio-generation/SKILL.md +92 -4
- package/project-template/.claude/skills/playcraft-level-editor/SKILL.md +34 -0
- package/project-template/.claude/skills/playcraft-level-editor/references/authoring-contract.json +3475 -0
- package/project-template/.claude/skills/playcraft-level-editor/references/extensions.md +37 -0
- package/project-template/.claude/skills/playcraft-level-editor/references/preview.md +15 -0
- package/project-template/.claude/skills/playcraft-level-editor/references/protocol-v1.md +35 -0
- package/project-template/vite.config.ts +1 -1
- package/project-template-v2/.claude/skills/playcraft-audio-generation/SKILL.md +92 -4
- package/project-template-v2/.playcraft/skills.lock.json +9 -2
- package/project-template-v2/vite.config.ts +1 -1
- package/project-templates/empty/AGENTS.md +2 -0
- package/project-templates/phaser/.claude/skills/playcraft-audio-generation/SKILL.md +92 -4
- package/project-templates/phaser/vite.config.ts +1 -1
- package/project-templates/playable-template-phaser/vite.config.ts +1 -1
- package/project-templates/threejs/.claude/skills/playcraft-audio-generation/SKILL.md +92 -4
- package/project-templates/threejs/vite.config.ts +1 -1
- package/templates/preview-runtime/v1/README.md +12 -0
- package/templates/preview-runtime/v1/build-preview.mjs +17 -8
- package/templates/preview-runtime/v1/manifest.json +9 -6
- package/templates/preview-runtime/v1/release-guard.mjs +1 -1
- package/templates/preview-runtime/v1/runtime.d.mts +1 -3
- package/templates/preview-runtime/v1/runtime.d.ts +1 -3
- package/templates/preview-runtime/v1/runtime.mjs +1142 -257
- package/templates/preview-runtime/v1/types/host.d.ts +29 -0
- package/templates/preview-runtime/v1/types/index.d.ts +6 -0
- package/templates/preview-runtime/v1/types/level-editor.d.ts +123 -0
|
@@ -1,23 +1,26 @@
|
|
|
1
|
+
import { takesWorkspaceDirectory } from '@playcraft/common/cloud-workspace/runtime';
|
|
2
|
+
import { assertNoPendingLevelEditorTransaction } from '../workspaces/level-editor-atomic.js';
|
|
1
3
|
import { withObservedFile } from '../workspaces/revisions.js';
|
|
2
|
-
import { revisionScope, sessionWorkspaceRoot, workspaceRoot, workspaceSessions } from '../workspaces/registry.js';
|
|
4
|
+
import { revisionScope, sessionWorkspaceRoot, workspaceRoot, workspaceSessions, } from '../workspaces/registry.js';
|
|
3
5
|
import { preflightSkillExecution } from '../workspaces/skill-execution-context.js';
|
|
4
|
-
import { abandonTurnChanges, beginTurnChanges, finishTurnChanges } from '../workspaces/turn-changes.js';
|
|
6
|
+
import { abandonTurnChanges, beginTurnChanges, finishTurnChanges, } from '../workspaces/turn-changes.js';
|
|
5
7
|
import { shareWorkspaceGit } from '../workspaces/shared-git.js';
|
|
6
|
-
import { cancelPendingDirectoryAccess, holdDirectoryAccess, withOperationDirectoryAccess } from './directory-access.js';
|
|
8
|
+
import { cancelPendingDirectoryAccess, holdDirectoryAccess, withOperationDirectoryAccess, } from './directory-access.js';
|
|
7
9
|
import { join } from 'node:path';
|
|
8
10
|
import { z } from 'zod';
|
|
9
|
-
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, workspaceModelPreviewBuildSchema, workspaceModelPreviewCancelSchema, } from '@playcraft/common/cloud-workspace/runtime';
|
|
11
|
+
import { fileWritePayloadSchema, fileBinaryWritePayloadSchema, directoryCreatePayloadSchema, directoryRenamePayloadSchema, directoryDeletePayloadSchema, directoryMutationReceiptSchema, gitCommitPayloadSchema, gitPathsPayloadSchema, gitPushPayloadSchema, gitPullPayloadSchema, gitMergeAbortPayloadSchema, hashOperationPayload, parameterPatchPayloadSchema, parameterAgentTaskRequestSchema, parameterAgentVerifyPayloadSchema, levelEditorTaskRequestSchema, levelEditorVerifyPayloadSchema, levelEditorTransactionRequestSchema, productionSessionAttemptId, reasoningEffortSchema, workspacePermissionModeSchema, workspaceComposerSelectionSchema, workspaceReviewPayloadSchema, workspaceCompactPayloadSchema, workspaceSteerPayloadSchema, workspaceApprovalResponseSchema, terminalCreatePayloadSchema, terminalProcessIdentity, transitionOperation, recoverUncertainOperation, workspaceSavePayloadSchema, workspaceSkillsUpdatePayloadSchema, workspaceRemixReferenceImportPayloadSchema, workspaceModelPreviewBuildSchema, workspaceModelPreviewCancelSchema, isSoundDocumentPath, } from '@playcraft/common/cloud-workspace/runtime';
|
|
10
12
|
import { closeCliCredentials, ensureCliCredentials } from '../workspaces/cli-credentials.js';
|
|
11
13
|
import { artifactFormat } from '../workspaces/artifact-files.js';
|
|
12
14
|
import { refreshWorkspaceArtifacts } from '../workspaces/artifact-events.js';
|
|
13
15
|
import { resolveWorkspaceAttachments } from '../workspaces/attachments.js';
|
|
14
|
-
import { importWorkspaceRemixReference, RemixReferenceError } from '../workspaces/remix-references.js';
|
|
16
|
+
import { importWorkspaceRemixReference, RemixReferenceError, } from '../workspaces/remix-references.js';
|
|
15
17
|
import { ComposerContextError, resolveComposerContext } from '../workspaces/composer-context.js';
|
|
18
|
+
import { workspaceAudioAssetService } from './audio-assets.js';
|
|
16
19
|
import { ensureParameterInitializationSkill } from '../workspaces/parameter-initialization-skill.js';
|
|
17
20
|
import { readWorkspaceSkillSnapshot, readWorkspaceSkills } from '../workspaces/skills.js';
|
|
18
|
-
import { loadWorkspaceSkillArtifact, workspacePlatformSkillAccess } from '../workspaces/skill-artifact-loader.js';
|
|
19
|
-
import { enableLocalProjectSkill, syncLocalProjectSkills } from '../../project-skills/local-store.js';
|
|
20
|
-
import { importBuiltinProjectSkills, migrateVideoProjectSkills } from '../../project-skills/builtin-import.js';
|
|
21
|
+
import { loadWorkspaceSkillArtifact, workspacePlatformSkillAccess, } from '../workspaces/skill-artifact-loader.js';
|
|
22
|
+
import { enableLocalProjectSkill, syncLocalProjectSkills, } from '../../project-skills/local-store.js';
|
|
23
|
+
import { importBuiltinProjectSkills, migrateVideoProjectSkills, } from '../../project-skills/builtin-import.js';
|
|
21
24
|
import { NativePreparationError } from '../codex/native-adapter.js';
|
|
22
25
|
import { workspaceUiSnapshotSchema } from '@playcraft/common/cloud-workspace/runtime';
|
|
23
26
|
import { disposeTerminalHost } from './terminal-host.js';
|
|
@@ -31,19 +34,23 @@ import { ThemeAssetIndexError } from '../workspaces/theme-asset-index.js';
|
|
|
31
34
|
import { patchWorkspaceParameters, readWorkspaceParameters } from '../workspaces/parameters.js';
|
|
32
35
|
import { prepareParameterAgentTask, finishParameterAgentTask, verifyParameterAgentTask, } from '../workspaces/parameter-agent.js';
|
|
33
36
|
import { previewFailureCode, previewWorkspaceRoot, runPreviewPublish, trackPreviewPublish, } from '../previews/publish.js';
|
|
34
|
-
import { startManagedBackgroundWriter, stopBackgroundWriter
|
|
37
|
+
import { startManagedBackgroundWriter, stopBackgroundWriter } from '../processes/managed-writes.js';
|
|
35
38
|
import { wakeSourceEventUploader } from './source-uploader.js';
|
|
36
39
|
import { batchDirectoryHeld } from '../batch/occupancy.js';
|
|
37
40
|
import { trustedMemberPolicy } from '../batch/policy.js';
|
|
38
|
-
import { BinaryWorkspaceWriteError, observeWorkspaceBinaryFile, writeWorkspaceBinaryFile } from '../workspaces/binary-write.js';
|
|
41
|
+
import { BinaryWorkspaceWriteError, observeWorkspaceBinaryFile, writeWorkspaceBinaryFile, } from '../workspaces/binary-write.js';
|
|
39
42
|
import { importWorkspaceLibraryAssets } from '../workspaces/asset-library-import.js';
|
|
40
|
-
import { createWorkspaceDirectory, WorkspaceDirectoryCreateError } from '../workspaces/directory-create.js';
|
|
41
|
-
import { DirectoryMutationError, mutateWorkspaceDirectory } from '../workspaces/directory-mutations.js';
|
|
43
|
+
import { createWorkspaceDirectory, WorkspaceDirectoryCreateError, } from '../workspaces/directory-create.js';
|
|
44
|
+
import { DirectoryMutationError, mutateWorkspaceDirectory, } from '../workspaces/directory-mutations.js';
|
|
42
45
|
import { observeWorkspaceDirectories } from '../workspaces/directory-revisions.js';
|
|
43
46
|
import { publishAssetTransfer } from '../workspaces/asset-transfer-publish.js';
|
|
44
47
|
import { AssetTransferError } from '../workspaces/asset-transfer-storage.js';
|
|
45
|
-
import { runModelPreviewBuild, cancelModelPreview, modelPreviewError } from '../workspaces/procedural-3d/model-preview-build.js';
|
|
48
|
+
import { runModelPreviewBuild, cancelModelPreview, modelPreviewError, } from '../workspaces/procedural-3d/model-preview-build.js';
|
|
46
49
|
import { runModelPreparation, trackModelPreparation } from '../workspaces/model-preparation.js';
|
|
50
|
+
import { prepareLevelEditorTask, finishLevelEditorTask, verifyLevelEditorTask, } from '../workspaces/level-editor-agent.js';
|
|
51
|
+
import { readWorkspaceLevelEditor, transactWorkspaceLevelEditor, reconcileWorkspaceLevelEditorTransaction, } from '../workspaces/level-editor.js';
|
|
52
|
+
import { readLevelEditorHistory } from '../workspaces/level-editor-history-store.js';
|
|
53
|
+
import { wakeLevelEditorRequests, ensureLevelEditorTaskPipeline, retryLevelEditorRequest, assertLevelEditorRetryInput, } from '../workspaces/level-editor-requests.js';
|
|
47
54
|
const CONVERSATION_KINDS = new Set([
|
|
48
55
|
'session.create',
|
|
49
56
|
'session.turn',
|
|
@@ -75,6 +82,9 @@ const LOCAL_KINDS = new Set([
|
|
|
75
82
|
'terminal.create',
|
|
76
83
|
'parameter.patch',
|
|
77
84
|
'parameter.verify',
|
|
85
|
+
'level-editor.request',
|
|
86
|
+
'level-editor.verify',
|
|
87
|
+
'level-editor.transaction',
|
|
78
88
|
'preview.publish',
|
|
79
89
|
'model.preview',
|
|
80
90
|
'model.preview.cancel',
|
|
@@ -94,7 +104,7 @@ export async function stopParameterVerifications(state) {
|
|
|
94
104
|
}
|
|
95
105
|
export async function recoverInterruptedParameterVerifications(journal) {
|
|
96
106
|
for (const { scopeKey, operation } of journal.listOperations()) {
|
|
97
|
-
if (
|
|
107
|
+
if (!['parameter.verify', 'level-editor.verify'].includes(operation.kind) || !['queued', 'dispatched', 'accepted', 'uncertain'].includes(operation.status))
|
|
98
108
|
continue;
|
|
99
109
|
if (operation.status === 'queued' && journal.deferredDirectoryOperations().some(item => item.operationId === operation.operationId))
|
|
100
110
|
continue;
|
|
@@ -116,9 +126,19 @@ export async function executeOperation(state, scopeKey, operation, payload, reco
|
|
|
116
126
|
if (operation.kind === 'session.turn' && !recovery && operation.sessionId &&
|
|
117
127
|
state.journal.getSession(operation.sessionId)?.recoverability === 'history_only')
|
|
118
128
|
return persist(state, scopeKey, transitionOperation(operation, 'failed', { error: 'native_not_recoverable' }));
|
|
119
|
-
return withOperationDirectoryAccess(state, operation, () => {
|
|
129
|
+
return withOperationDirectoryAccess(state, operation, async () => {
|
|
120
130
|
if ((operation.leaseFence ?? 0) < state.acceptedLeaseFence || state.journal.destroyed())
|
|
121
131
|
return persist(state, scopeKey, transitionOperation(operation, 'failed', { error: state.journal.destroyed() ? 'runtime_destroyed' : 'stale_lease' }));
|
|
132
|
+
if (takesWorkspaceDirectory(operation.kind) && operation.kind !== 'level-editor.transaction' && operation.kind !== 'runtime.destroy') {
|
|
133
|
+
// `root` is the workspace container, not a registered directory: resolving it must go
|
|
134
|
+
// through the session root, or every root-scoped operation fails before it starts.
|
|
135
|
+
try {
|
|
136
|
+
await assertNoPendingLevelEditorTransaction(await sessionWorkspaceRoot(state, operation.workspaceId, operation.directoryName), { state, operation });
|
|
137
|
+
}
|
|
138
|
+
catch (error) {
|
|
139
|
+
return persist(state, scopeKey, transitionOperation(operation, 'failed', { error: levelEditorErrorCode(error) }));
|
|
140
|
+
}
|
|
141
|
+
}
|
|
122
142
|
return executeScopedOperation(state, scopeKey, operation, payload, recovery);
|
|
123
143
|
}, { operationId: operation.operationId, scopeKey, payload, ...(recovery ? { recovery } : {}) });
|
|
124
144
|
};
|
|
@@ -195,6 +215,23 @@ async function executeScopedOperation(state, scopeKey, operation, payload, recov
|
|
|
195
215
|
if (operation.kind === 'parameter.verify') {
|
|
196
216
|
return executeParameterVerify(state, scopeKey, current, payload);
|
|
197
217
|
}
|
|
218
|
+
if (operation.kind === 'level-editor.verify')
|
|
219
|
+
return executeLevelEditorVerify(state, scopeKey, current, payload);
|
|
220
|
+
if (operation.kind === 'level-editor.transaction')
|
|
221
|
+
return executeLevelEditorTransaction(state, scopeKey, current, payload);
|
|
222
|
+
if (operation.kind === 'level-editor.request') {
|
|
223
|
+
try {
|
|
224
|
+
const root = await workspaceRoot(state, current.workspaceId, current.directoryName);
|
|
225
|
+
const result = await retryLevelEditorRequest(state, current, payload, root);
|
|
226
|
+
const accepted = await persist(state, scopeKey, transitionOperation(current, 'accepted'));
|
|
227
|
+
const completed = await persist(state, scopeKey, transitionOperation(accepted, 'completed', { result }));
|
|
228
|
+
wakeLevelEditorRequests(state);
|
|
229
|
+
return completed;
|
|
230
|
+
}
|
|
231
|
+
catch (error) {
|
|
232
|
+
return persist(state, scopeKey, transitionOperation(current, 'failed', { error: levelEditorErrorCode(error) }));
|
|
233
|
+
}
|
|
234
|
+
}
|
|
198
235
|
if (operation.kind === 'preview.publish') {
|
|
199
236
|
return executePreviewPublish(state, scopeKey, current);
|
|
200
237
|
}
|
|
@@ -399,11 +436,14 @@ async function executeTurn(state, scopeKey, operation, payload, recovery) {
|
|
|
399
436
|
const parameterTask = parameterAgentTaskRequestSchema.optional().safeParse(body.parameterTask);
|
|
400
437
|
if (!parameterTask.success || (parameterTask.data && parameterTask.data.taskId !== operation.clientRequestId))
|
|
401
438
|
return persist(state, scopeKey, transitionOperation(operation, 'failed', { error: 'invalid_parameter_task' }));
|
|
402
|
-
|
|
439
|
+
const levelEditorTask = levelEditorTaskRequestSchema.optional().safeParse(body.levelEditorTask);
|
|
440
|
+
if (!levelEditorTask.success || levelEditorTask.data && (parameterTask.data || levelEditorTask.data.taskId !== operation.clientRequestId))
|
|
441
|
+
return persist(state, scopeKey, transitionOperation(operation, 'failed', { error: 'invalid_level_editor_task' }));
|
|
442
|
+
if (parameterTask.data || levelEditorTask.data) {
|
|
403
443
|
if (operation.directoryName === 'root')
|
|
404
444
|
return persist(state, scopeKey, transitionOperation(operation, 'failed', { error: 'invalid_workspace_directory' }));
|
|
405
445
|
if ((selection.data.workMode ?? 'execute') !== 'execute' || requestedMode === 'read-only')
|
|
406
|
-
return persist(state, scopeKey, transitionOperation(operation, 'failed', { error: 'parameter_task_readonly' }));
|
|
446
|
+
return persist(state, scopeKey, transitionOperation(operation, 'failed', { error: levelEditorTask.data ? 'level_editor_task_requires_execute' : 'parameter_task_readonly' }));
|
|
407
447
|
if (workspaceSessions(state, operation.workspaceId, operation.directoryName).some(sessionTurnBusy))
|
|
408
448
|
return persist(state, scopeKey, transitionOperation(operation, 'failed', { error: 'turn_already_active' }));
|
|
409
449
|
if (await batchDirectoryHeld(state, operation.workspaceId, operation.directoryName))
|
|
@@ -439,7 +479,7 @@ async function executeTurn(state, scopeKey, operation, payload, recovery) {
|
|
|
439
479
|
if (policy?.directoryAccessMode === 'exclusive' && operation.directoryAccessMode !== 'exclusive')
|
|
440
480
|
throw new ComposerContextError('requires-exclusive-turn');
|
|
441
481
|
if (operation.skillExecutionPolicyVersion === 1 && (!policy ||
|
|
442
|
-
(parameterTask.data ? 'exclusive' : policy.directoryAccessMode) !== operation.directoryAccessMode))
|
|
482
|
+
(parameterTask.data || levelEditorTask.data ? 'exclusive' : policy.directoryAccessMode) !== operation.directoryAccessMode))
|
|
443
483
|
throw new ComposerContextError('context_skills_changed');
|
|
444
484
|
}
|
|
445
485
|
catch (error) {
|
|
@@ -452,8 +492,8 @@ async function executeTurn(state, scopeKey, operation, payload, recovery) {
|
|
|
452
492
|
let initializationSkill;
|
|
453
493
|
try {
|
|
454
494
|
let turnContext = selection.data.context;
|
|
455
|
-
if (parameterTask.data?.action === 'initialize') {
|
|
456
|
-
const selectedSkill = await ensureParameterInitializationSkill(root);
|
|
495
|
+
if (parameterTask.data?.action === 'initialize' || levelEditorTask.data) {
|
|
496
|
+
const selectedSkill = await ensureParameterInitializationSkill(root, levelEditorTask.data ? 'playcraft-level-editor' : 'playcraft-runtime-parameters', levelEditorTask.data ? { preserveExisting: true } : {});
|
|
457
497
|
initializationSkill = { ...selectedSkill,
|
|
458
498
|
path: join(root, '.playcraft/skills', selectedSkill.lockHash, selectedSkill.entryKey, 'SKILL.md') };
|
|
459
499
|
turnContext = { ...turnContext, files: turnContext?.files ?? [], skills: [
|
|
@@ -465,13 +505,16 @@ async function executeTurn(state, scopeKey, operation, payload, recovery) {
|
|
|
465
505
|
directoryName: binding.directoryName ?? 'basic', context: turnContext,
|
|
466
506
|
skillCache: join(state.config.stateDir, 'skill-artifacts'),
|
|
467
507
|
loadSkillArtifact: entry => loadWorkspaceSkillArtifact(state, operation.workspaceId ?? 'workspace', entry),
|
|
508
|
+
...(process.env.CLOUD_WORKSPACE_AUDIO_ASSETS_ENABLED === 'true' ? {
|
|
509
|
+
readAudioTarget: async (target) => (await workspaceAudioAssetService(state, operation.workspaceId, binding.directoryName ?? 'basic')).read(target),
|
|
510
|
+
} : {}),
|
|
468
511
|
});
|
|
469
512
|
}
|
|
470
513
|
catch (error) {
|
|
471
514
|
return persist(state, scopeKey, transitionOperation(operation, 'failed', {
|
|
472
|
-
error: error instanceof ComposerContextError ? error.message : parameterTask.data?.action === 'initialize'
|
|
515
|
+
error: error instanceof ComposerContextError ? error.message : parameterTask.data?.action === 'initialize' || levelEditorTask.data
|
|
473
516
|
? error instanceof Error && /^(?:workspace_skills_|skills_revision_conflict$)/.test(error.message)
|
|
474
|
-
? error.message : parameterAgentError(error)
|
|
517
|
+
? error.message : levelEditorTask.data ? 'level_editor_skill_unavailable' : parameterAgentError(error)
|
|
475
518
|
: 'invalid_composer_context',
|
|
476
519
|
}));
|
|
477
520
|
}
|
|
@@ -489,14 +532,12 @@ async function executeTurn(state, scopeKey, operation, payload, recovery) {
|
|
|
489
532
|
})).join('\n') : '';
|
|
490
533
|
const batchContext = operation.directoryName === 'root' && operation.workspaceId
|
|
491
534
|
? `PlayCraft batch request context (trusted runtime identity): ${JSON.stringify({ workspaceId: operation.workspaceId,
|
|
492
|
-
clientRequestId: operation.clientRequestId, sourceDirectoryName: 'basic'
|
|
493
|
-
...(typeof body.teamId === 'string' ? { teamId: body.teamId } : {}) })}\n\n`
|
|
535
|
+
clientRequestId: operation.clientRequestId, sourceDirectoryName: 'basic' })}\n\n`
|
|
494
536
|
: '';
|
|
495
537
|
const engineeringBatchContext = state.config.workspaceMode === 'multi' && operation.workspaceId
|
|
496
538
|
&& operation.directoryName !== 'root' && !productionSessionAttemptId(sessionId) && !trustedMemberPolicy(state, sessionId)
|
|
497
539
|
? `PlayCraft batch request context (trusted runtime identity): ${JSON.stringify({ workspaceId: operation.workspaceId,
|
|
498
|
-
clientRequestId: operation.clientRequestId, sourceDirectoryName: binding.directoryName ?? 'basic'
|
|
499
|
-
...(typeof body.teamId === 'string' ? { teamId: body.teamId } : {}) })}`
|
|
540
|
+
clientRequestId: operation.clientRequestId, sourceDirectoryName: binding.directoryName ?? 'basic' })}`
|
|
500
541
|
: '';
|
|
501
542
|
if (state.config.workspaceMode === 'multi') {
|
|
502
543
|
try {
|
|
@@ -516,6 +557,14 @@ async function executeTurn(state, scopeKey, operation, payload, recovery) {
|
|
|
516
557
|
return persist(state, scopeKey, transitionOperation(operation, 'failed', { error: parameterAgentError(error) }));
|
|
517
558
|
}
|
|
518
559
|
}
|
|
560
|
+
if (levelEditorTask.data) {
|
|
561
|
+
try {
|
|
562
|
+
parameterInstructions = (await prepareLevelEditorTask(state, operation, levelEditorTask.data, root, initializationSkill)).instructions;
|
|
563
|
+
}
|
|
564
|
+
catch (error) {
|
|
565
|
+
return persist(state, scopeKey, transitionOperation(operation, 'failed', { error: levelEditorErrorCode(error) }));
|
|
566
|
+
}
|
|
567
|
+
}
|
|
519
568
|
// Taken before the native turn exists, so nothing the agent writes can precede it.
|
|
520
569
|
await beginTurnChanges(state, sessionId, {
|
|
521
570
|
root, workspaceId: operation.workspaceId, directoryName: binding.directoryName ?? 'basic',
|
|
@@ -548,7 +597,7 @@ async function executeTurn(state, scopeKey, operation, payload, recovery) {
|
|
|
548
597
|
permissionMode: selectedMode,
|
|
549
598
|
workMode: selection.data.workMode ?? 'execute',
|
|
550
599
|
context: composer.context,
|
|
551
|
-
userText: selection.data.workMode !== undefined || composer.context !== undefined || parameterTask.data
|
|
600
|
+
userText: selection.data.workMode !== undefined || composer.context !== undefined || parameterTask.data || levelEditorTask.data
|
|
552
601
|
? String(body.userText ?? body.text ?? '') : undefined,
|
|
553
602
|
updatedAt: new Date().toISOString(),
|
|
554
603
|
});
|
|
@@ -569,7 +618,7 @@ async function executeTurn(state, scopeKey, operation, payload, recovery) {
|
|
|
569
618
|
}
|
|
570
619
|
}
|
|
571
620
|
catch (error) {
|
|
572
|
-
if (!parameterTask.data)
|
|
621
|
+
if (!parameterTask.data && !levelEditorTask.data)
|
|
573
622
|
throw error;
|
|
574
623
|
abandonTurnChanges(state, sessionId);
|
|
575
624
|
if (error instanceof NativePreparationError) {
|
|
@@ -631,7 +680,7 @@ async function executeTurn(state, scopeKey, operation, payload, recovery) {
|
|
|
631
680
|
threadId: binding.threadId,
|
|
632
681
|
turnId: result.turnId,
|
|
633
682
|
});
|
|
634
|
-
if (selection.data.workMode !== undefined || composer.context !== undefined || parameterTask.data) {
|
|
683
|
+
if (selection.data.workMode !== undefined || composer.context !== undefined || parameterTask.data || levelEditorTask.data) {
|
|
635
684
|
await recordNativeEvents(state, accepted, [{
|
|
636
685
|
type: 'composer.context', occurredAt: new Date().toISOString(), payload: {
|
|
637
686
|
threadId: binding.threadId, turnId: result.turnId, clientRequestId: operation.clientRequestId,
|
|
@@ -1168,6 +1217,8 @@ async function executeFileBinaryWrite(state, scopeKey, operation, payload) {
|
|
|
1168
1217
|
if (!parsed.success)
|
|
1169
1218
|
return persist(state, scopeKey, transitionOperation(operation, 'failed', { error: 'invalid_binary_write' }));
|
|
1170
1219
|
const input = parsed.data;
|
|
1220
|
+
if (isSoundDocumentPath(input.path))
|
|
1221
|
+
return persist(state, scopeKey, transitionOperation(operation, 'failed', { error: 'unsupported_audio_protocol' }));
|
|
1171
1222
|
const scope = revisionScope(state, operation.workspaceId, operation.directoryName);
|
|
1172
1223
|
return state.journal.exclusive(`file:${scope ?? 'single'}:${input.path}`, async () => {
|
|
1173
1224
|
try {
|
|
@@ -1211,6 +1262,8 @@ async function executeFileBinaryWrite(state, scopeKey, operation, payload) {
|
|
|
1211
1262
|
}
|
|
1212
1263
|
async function executeFileWrite(state, scopeKey, operation, payload) {
|
|
1213
1264
|
const parsed = fileWritePayloadSchema.safeParse(payload);
|
|
1265
|
+
if (parsed.success && isSoundDocumentPath(parsed.data.path))
|
|
1266
|
+
return persist(state, scopeKey, transitionOperation(operation, 'failed', { error: 'unsupported_audio_protocol' }));
|
|
1214
1267
|
if (!parsed.success) {
|
|
1215
1268
|
const message = parsed.error.issues[0]?.message;
|
|
1216
1269
|
const error = message === 'write_id_required' || message === 'write_hold_conflict' || message === 'background_write_unmanaged'
|
|
@@ -1438,6 +1491,172 @@ async function executeGitIndex(state, scopeKey, operation, payload) {
|
|
|
1438
1491
|
return persist(state, scopeKey, transitionOperation(operation, 'failed', { error: gitErrorCode(error) }));
|
|
1439
1492
|
}
|
|
1440
1493
|
}
|
|
1494
|
+
async function withLevelEditorDocuments(state, operation, paths, action) {
|
|
1495
|
+
const observed = [...new Set(paths)].sort();
|
|
1496
|
+
const run = (index) => index === observed.length
|
|
1497
|
+
? action()
|
|
1498
|
+
: withObservedFile(state, operation.workspaceId, observed[index], () => run(index + 1), operation.directoryName);
|
|
1499
|
+
return run(0);
|
|
1500
|
+
}
|
|
1501
|
+
async function executeLevelEditorTransaction(state, scopeKey, operation, payload) {
|
|
1502
|
+
const parsed = levelEditorTransactionRequestSchema.safeParse(payload);
|
|
1503
|
+
if (!parsed.success || parsed.data.requestId !== operation.clientRequestId)
|
|
1504
|
+
return persist(state, scopeKey, transitionOperation(operation, 'failed', { error: 'invalid_level_editor_transaction' }));
|
|
1505
|
+
const request = parsed.data;
|
|
1506
|
+
const root = await workspaceRoot(state, operation.workspaceId, operation.directoryName), scope = revisionScope(state, operation.workspaceId, operation.directoryName);
|
|
1507
|
+
const snapshot = await readWorkspaceLevelEditor(root, state.journal.fileRevisions(scope), {
|
|
1508
|
+
state,
|
|
1509
|
+
operation,
|
|
1510
|
+
});
|
|
1511
|
+
const ids = request.version === 1 ? [request.documentId] : Object.keys(request.expectedDocuments);
|
|
1512
|
+
const paths = ids.map((id) => snapshot.definition?.documents[id]?.path);
|
|
1513
|
+
if (paths.some((path) => !path) || !paths.length)
|
|
1514
|
+
return persist(state, scopeKey, transitionOperation(operation, 'failed', {
|
|
1515
|
+
error: snapshot.diagnostics[0]?.code ?? 'level_editor_not_ready',
|
|
1516
|
+
}));
|
|
1517
|
+
return withLevelEditorDocuments(state, operation, paths, async () => {
|
|
1518
|
+
let applied = false;
|
|
1519
|
+
try {
|
|
1520
|
+
const written = await transactWorkspaceLevelEditor(root, state.journal.fileRevisions(scope), request, { state, operation });
|
|
1521
|
+
applied = true;
|
|
1522
|
+
const files = written.results ?? [
|
|
1523
|
+
{ ...written, documentId: request.version === 1 ? request.documentId : '' },
|
|
1524
|
+
];
|
|
1525
|
+
for (const file of files)
|
|
1526
|
+
await state.journal.putFileRevision(file.path, { revision: file.revision, contentHash: file.contentHash }, scope);
|
|
1527
|
+
const revision = Math.max(...files.map((file) => file.revision));
|
|
1528
|
+
const accepted = await persist(state, scopeKey, transitionOperation(operation, 'accepted', { expectedRevision: revision }));
|
|
1529
|
+
await recordNativeEvents(state, accepted, files.map((file) => ({
|
|
1530
|
+
type: 'file.written',
|
|
1531
|
+
occurredAt: new Date().toISOString(),
|
|
1532
|
+
payload: { ...file, editorId: request.editorId },
|
|
1533
|
+
})));
|
|
1534
|
+
return persist(state, scopeKey, transitionOperation(accepted, 'completed', {
|
|
1535
|
+
expectedRevision: revision,
|
|
1536
|
+
result: await readWorkspaceLevelEditor(root, state.journal.fileRevisions(scope), {
|
|
1537
|
+
state,
|
|
1538
|
+
operation,
|
|
1539
|
+
}),
|
|
1540
|
+
}));
|
|
1541
|
+
}
|
|
1542
|
+
catch (error) {
|
|
1543
|
+
const code = applied ? 'level_editor_transaction_uncertain' : levelEditorErrorCode(error);
|
|
1544
|
+
return persist(state, scopeKey, transitionOperation(operation, code === 'level_editor_transaction_uncertain' ? 'uncertain' : 'failed', { error: code }));
|
|
1545
|
+
}
|
|
1546
|
+
});
|
|
1547
|
+
}
|
|
1548
|
+
/** Private receipts prove completion; a marker can only resume the previously authorized exact batch. */
|
|
1549
|
+
export async function reconcileLevelEditorTransaction(state, scopeKey, operation, payload) {
|
|
1550
|
+
if (operation.kind !== 'level-editor.transaction' ||
|
|
1551
|
+
!['dispatched', 'accepted', 'uncertain'].includes(operation.status) ||
|
|
1552
|
+
state.journal.destroyed())
|
|
1553
|
+
return undefined;
|
|
1554
|
+
const result = await state.journal.exclusive(`workspace:${revisionScope(state, operation.workspaceId, operation.directoryName) ?? 'single'}`, () => withOperationDirectoryAccess(state, operation, async () => {
|
|
1555
|
+
const root = await workspaceRoot(state, operation.workspaceId, operation.directoryName);
|
|
1556
|
+
const trusted = payload === undefined
|
|
1557
|
+
? await readLevelEditorHistory(root, { state, operation }, operation.clientRequestId)
|
|
1558
|
+
: undefined;
|
|
1559
|
+
const parsed = levelEditorTransactionRequestSchema.safeParse(payload ?? trusted?.request);
|
|
1560
|
+
if (!parsed.success ||
|
|
1561
|
+
parsed.data.requestId !== operation.clientRequestId ||
|
|
1562
|
+
hashOperationPayload(parsed.data) !== operation.payloadHash)
|
|
1563
|
+
return operation;
|
|
1564
|
+
const request = parsed.data, scope = revisionScope(state, operation.workspaceId, operation.directoryName);
|
|
1565
|
+
const initial = await readWorkspaceLevelEditor(root, state.journal.fileRevisions(scope), {
|
|
1566
|
+
state,
|
|
1567
|
+
operation,
|
|
1568
|
+
});
|
|
1569
|
+
const ids = request.version === 1 ? [request.documentId] : Object.keys(request.expectedDocuments);
|
|
1570
|
+
const paths = ids.map((id) => initial.definition?.documents[id]?.path);
|
|
1571
|
+
if (paths.some((path) => !path))
|
|
1572
|
+
return operation;
|
|
1573
|
+
return withLevelEditorDocuments(state, operation, paths, async () => {
|
|
1574
|
+
const proof = await reconcileWorkspaceLevelEditorTransaction(root, state.journal.fileRevisions(scope), request, { state, operation });
|
|
1575
|
+
if (!proof)
|
|
1576
|
+
return operation;
|
|
1577
|
+
const files = proof.results ?? [
|
|
1578
|
+
{
|
|
1579
|
+
path: proof.path,
|
|
1580
|
+
contentHash: proof.contentHash,
|
|
1581
|
+
size: proof.size,
|
|
1582
|
+
revision: proof.revision,
|
|
1583
|
+
documentId: request.version === 1 ? request.documentId : '',
|
|
1584
|
+
},
|
|
1585
|
+
];
|
|
1586
|
+
for (const file of files) {
|
|
1587
|
+
await state.journal.putFileRevision(file.path, { revision: file.revision, contentHash: file.contentHash }, scope);
|
|
1588
|
+
const duplicate = state.journal.listEvents().some((event) => {
|
|
1589
|
+
const body = asRecord(event.payload);
|
|
1590
|
+
return (body.operationId === operation.operationId &&
|
|
1591
|
+
event.type === 'file.written' &&
|
|
1592
|
+
body.contentHash === file.contentHash &&
|
|
1593
|
+
body.revision === file.revision &&
|
|
1594
|
+
body.documentId === file.documentId);
|
|
1595
|
+
});
|
|
1596
|
+
if (!duplicate)
|
|
1597
|
+
await recordNativeEvents(state, operation, [
|
|
1598
|
+
{
|
|
1599
|
+
type: 'file.written',
|
|
1600
|
+
occurredAt: new Date().toISOString(),
|
|
1601
|
+
payload: { ...file, editorId: request.editorId },
|
|
1602
|
+
},
|
|
1603
|
+
]);
|
|
1604
|
+
}
|
|
1605
|
+
const current = operation.status === 'dispatched'
|
|
1606
|
+
? transitionOperation(operation, 'accepted')
|
|
1607
|
+
: operation;
|
|
1608
|
+
return persist(state, scopeKey, transitionOperation(current, 'completed', {
|
|
1609
|
+
expectedRevision: Math.max(...files.map((file) => file.revision)),
|
|
1610
|
+
result: proof.snapshot,
|
|
1611
|
+
error: undefined,
|
|
1612
|
+
}));
|
|
1613
|
+
});
|
|
1614
|
+
}));
|
|
1615
|
+
return result.status === 'completed' ? result : undefined;
|
|
1616
|
+
}
|
|
1617
|
+
async function executeLevelEditorVerify(state, scopeKey, operation, payload) {
|
|
1618
|
+
const parsed = levelEditorVerifyPayloadSchema.safeParse(payload);
|
|
1619
|
+
if (!parsed.success)
|
|
1620
|
+
return persist(state, scopeKey, transitionOperation(operation, 'failed', { error: 'invalid_level_editor_task' }));
|
|
1621
|
+
if (operation.directoryName === 'root' || workspaceSessions(state, operation.workspaceId, operation.directoryName).some(sessionTurnBusy))
|
|
1622
|
+
return persist(state, scopeKey, transitionOperation(operation, 'failed', { error: 'turn_already_active' }));
|
|
1623
|
+
if (await batchDirectoryHeld(state, operation.workspaceId, operation.directoryName))
|
|
1624
|
+
return persist(state, scopeKey, transitionOperation(operation, 'failed', { error: 'batch_workspace_readonly' }));
|
|
1625
|
+
const root = await workspaceRoot(state, operation.workspaceId, operation.directoryName);
|
|
1626
|
+
const accepted = await persist(state, scopeKey, transitionOperation(operation, 'accepted'));
|
|
1627
|
+
const controller = new AbortController();
|
|
1628
|
+
const assertActive = () => {
|
|
1629
|
+
if (controller.signal.aborted ||
|
|
1630
|
+
parameterVerificationClosing.has(state) ||
|
|
1631
|
+
state.journal.destroyed())
|
|
1632
|
+
throw new Error('level_editor_verification_interrupted');
|
|
1633
|
+
if ((operation.leaseFence ?? 0) < state.acceptedLeaseFence)
|
|
1634
|
+
throw new Error('stale_lease');
|
|
1635
|
+
};
|
|
1636
|
+
const key = `workspace:${revisionScope(state, operation.workspaceId, operation.directoryName) ?? 'single'}`;
|
|
1637
|
+
const task = state.journal.exclusive(key, async () => {
|
|
1638
|
+
try {
|
|
1639
|
+
await assertLevelEditorRetryInput(state, operation, parsed.data, root);
|
|
1640
|
+
const result = await verifyLevelEditorTask(state, operation, parsed.data, root, {
|
|
1641
|
+
assertActive,
|
|
1642
|
+
signal: controller.signal,
|
|
1643
|
+
});
|
|
1644
|
+
assertActive();
|
|
1645
|
+
await persist(state, scopeKey, transitionOperation(accepted, 'completed', { result }));
|
|
1646
|
+
}
|
|
1647
|
+
catch (error) {
|
|
1648
|
+
await persist(state, scopeKey, transitionOperation(accepted, 'failed', { error: levelEditorErrorCode(error) }));
|
|
1649
|
+
}
|
|
1650
|
+
});
|
|
1651
|
+
const tasks = parameterVerifications.get(state) ?? new Map();
|
|
1652
|
+
parameterVerifications.set(state, tasks);
|
|
1653
|
+
const tracked = task.catch(() => undefined).finally(() => tasks.delete(controller));
|
|
1654
|
+
tasks.set(controller, tracked);
|
|
1655
|
+
return accepted;
|
|
1656
|
+
}
|
|
1657
|
+
function levelEditorErrorCode(error) {
|
|
1658
|
+
return error instanceof Error && /^(?:level_editor_|invalid_level_editor_|revision_conflict|stale_lease|runtime_destroyed)/.test(error.message) ? error.message : 'level_editor_operation_failed';
|
|
1659
|
+
}
|
|
1441
1660
|
async function executeParameterPatch(state, scopeKey, operation, payload) {
|
|
1442
1661
|
const parsed = parameterPatchPayloadSchema.safeParse(payload);
|
|
1443
1662
|
if (!parsed.success) {
|
|
@@ -1729,6 +1948,10 @@ async function syncParameterAgentOperation(state, operation) {
|
|
|
1729
1948
|
return;
|
|
1730
1949
|
if (operation.status === 'completed' || operation.status === 'failed' || operation.status === 'interrupted' || operation.status === 'uncertain') {
|
|
1731
1950
|
await finishParameterAgentTask(state, operation, operation.status, operation.error);
|
|
1951
|
+
await finishLevelEditorTask(state, operation, operation.status, operation.error);
|
|
1952
|
+
if (operation.status === 'completed' && /^[a-f0-9-]{36}$/i.test(operation.clientRequestId))
|
|
1953
|
+
await ensureLevelEditorTaskPipeline(state, operation);
|
|
1954
|
+
wakeLevelEditorRequests(state);
|
|
1732
1955
|
}
|
|
1733
1956
|
}
|
|
1734
1957
|
function native(state) {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { hasPendingLevelEditorTransaction, assertNoPendingLevelEditorTransaction } from '../workspaces/level-editor-atomic.js';
|
|
1
2
|
import { readObservedFile, withObservedFile } from '../workspaces/revisions.js';
|
|
2
3
|
import { observeWorkspaceDirectories } from '../workspaces/directory-revisions.js';
|
|
3
4
|
import { registerWorkspace, revisionScope, sessionWorkspaceRoot, workspaceRoot, WorkspaceContextError } from '../workspaces/registry.js';
|
|
@@ -11,6 +12,9 @@ import { InvalidWorkspacePathError, } from '../workspaces/files.js';
|
|
|
11
12
|
import { GitWorkspaceError, readWorkspaceGitDiff, readWorkspaceGitFile, readWorkspaceGitGraph, readWorkspaceGitStatus, } from '../workspaces/git.js';
|
|
12
13
|
import { readWorkspaceParameters } from '../workspaces/parameters.js';
|
|
13
14
|
import { listParameterAgentTasks } from '../workspaces/parameter-agent.js';
|
|
15
|
+
import { readWorkspaceLevelEditor } from '../workspaces/level-editor.js';
|
|
16
|
+
import { listLevelEditorTasks } from '../workspaces/level-editor-agent.js';
|
|
17
|
+
import { listLevelEditorRequests } from '../workspaces/level-editor-requests.js';
|
|
14
18
|
import { readDependencies } from '../workspaces/dependencies.js';
|
|
15
19
|
import { permissionDriftFromError } from '../workspaces/workspace-shares.js';
|
|
16
20
|
import { desktopHost, desktopRoute } from './desktop-host.js';
|
|
@@ -36,7 +40,7 @@ import { probeRuntimeConnectivity } from './connectivity.js';
|
|
|
36
40
|
import { refreshWorkspaceArtifacts } from '../workspaces/artifact-events.js';
|
|
37
41
|
import { WorkspaceArtifactStore } from '../workspaces/artifacts.js';
|
|
38
42
|
import { OperationLogStore } from '../workspaces/operation-log.js';
|
|
39
|
-
import { acceptOperationCore } from './accept-operation.js';
|
|
43
|
+
import { acceptOperationCore, observeLevelEditorTransaction } from './accept-operation.js';
|
|
40
44
|
import { workspaceArtifactContentQuerySchema } from '@playcraft/common/cloud-workspace/runtime';
|
|
41
45
|
import { searchWorkspaceFiles } from '../workspaces/files-search.js';
|
|
42
46
|
import { serveWorkspaceArchive } from '../workspaces/archive.js';
|
|
@@ -50,11 +54,43 @@ import { withDirectoryWrite } from './directory-access.js';
|
|
|
50
54
|
import { readProceduralModelCandidates } from '../workspaces/procedural-3d/read-candidates.js';
|
|
51
55
|
import { ModelPreviewStore } from '../workspaces/procedural-3d/model-preview-store.js';
|
|
52
56
|
import { readModelPreparation } from '../workspaces/model-preparation.js';
|
|
57
|
+
import { serveAudioExport } from './audio-export.js';
|
|
58
|
+
import { handleAudioAssetRequest } from './audio-assets.js';
|
|
53
59
|
export async function handleRuntimeRequest(state, request, response) {
|
|
54
60
|
const url = new URL(request.url ?? '/', 'http://127.0.0.1');
|
|
55
61
|
const workspaceId = url.searchParams.get('workspaceId');
|
|
62
|
+
const protectedRead = ['/runtime/level-editor', '/runtime/parameters', '/runtime/file', '/runtime/files', '/runtime/files/search', '/runtime/file-content', '/runtime/archive', '/runtime/git-bundle', '/runtime/git/status', '/runtime/git/diff', '/runtime/audio/assets', '/runtime/audio/content'].includes(url.pathname) && ['GET', 'HEAD'].includes(request.method ?? 'GET');
|
|
63
|
+
if (authorizeRuntime(state, request) === 'control' && protectedRead) {
|
|
64
|
+
try {
|
|
65
|
+
const directoryName = url.searchParams.get('directoryName') ?? 'basic';
|
|
66
|
+
const readContext = { state, workspaceId: workspaceId ?? undefined, directoryName };
|
|
67
|
+
const root = await workspaceRoot(state, readContext.workspaceId, directoryName);
|
|
68
|
+
if (await hasPendingLevelEditorTransaction(root, readContext)) {
|
|
69
|
+
// A retained writer owns the exclusive lease. Reconcile that exact operation before admitting a reader.
|
|
70
|
+
for (const { operation } of state.journal.listOperations())
|
|
71
|
+
if (operation.kind === 'level-editor.transaction' && (state.config.workspaceMode !== 'multi' || operation.workspaceId === readContext.workspaceId) && (operation.directoryName ?? 'basic') === directoryName)
|
|
72
|
+
await observeLevelEditorTransaction(state, operation);
|
|
73
|
+
}
|
|
74
|
+
const recovering = await hasPendingLevelEditorTransaction(root, readContext);
|
|
75
|
+
return await withDirectoryWrite(state, { ...readContext, mode: recovering ? 'exclusive' : 'shared', wait: false }, async () => {
|
|
76
|
+
if (recovering) {
|
|
77
|
+
const snapshot = await readWorkspaceLevelEditor(root, state.journal.fileRevisions(revisionScope(state, readContext.workspaceId, directoryName)), readContext);
|
|
78
|
+
if (snapshot.status === 'invalid' && snapshot.diagnostics.some(item => /recovery|history/.test(item.code)))
|
|
79
|
+
throw new Error(snapshot.diagnostics[0].code);
|
|
80
|
+
}
|
|
81
|
+
await assertNoPendingLevelEditorTransaction(root, readContext);
|
|
82
|
+
return dispatchRuntimeRequest(state, request, response);
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
if (!response.headersSent)
|
|
87
|
+
writeJson(response, 409, { error: error instanceof Error ? error.message : 'level_editor_recovery_required' });
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
56
91
|
// Body-scoped operations, members and terminal control take their own lifetime-aware leases.
|
|
57
92
|
const ownLease = url.pathname.startsWith('/runtime/operations') || url.pathname.startsWith('/runtime/terminals/') ||
|
|
93
|
+
url.pathname === '/runtime/audio/export' ||
|
|
58
94
|
url.pathname.startsWith('/runtime/batch-') || /^\/runtime\/sessions\/[^/]+\/ui-results$/.test(url.pathname) ||
|
|
59
95
|
/^\/runtime\/sessions\/[^/]+\/prediction$/.test(url.pathname);
|
|
60
96
|
if (authorizeRuntime(state, request) === 'control' && workspaceId &&
|
|
@@ -87,6 +123,15 @@ async function dispatchRuntimeRequest(state, request, response) {
|
|
|
87
123
|
return;
|
|
88
124
|
}
|
|
89
125
|
try {
|
|
126
|
+
if (request.method === 'POST' && url.pathname === '/runtime/audio/export') {
|
|
127
|
+
if (writeDestroyed(state, response))
|
|
128
|
+
return;
|
|
129
|
+
await sessionWorkspaceRoot(state, url.searchParams.get('workspaceId') ?? undefined, directoryName);
|
|
130
|
+
await serveAudioExport(request, response, url);
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
if (await handleAudioAssetRequest(state, request, response, url))
|
|
134
|
+
return;
|
|
90
135
|
if (await handleWorkspaceStorage(state, request, response, url))
|
|
91
136
|
return;
|
|
92
137
|
const transfer = await handleAssetTransferRequest(state, request, url);
|
|
@@ -135,6 +180,10 @@ async function dispatchRuntimeRequest(state, request, response) {
|
|
|
135
180
|
instanceToken: state.instanceToken,
|
|
136
181
|
permissionsVersion: 1,
|
|
137
182
|
composerContextVersion: 1,
|
|
183
|
+
audioAssetVersion: 1,
|
|
184
|
+
audioContextVersion: 1,
|
|
185
|
+
audioScoreVersion: 1,
|
|
186
|
+
audioScoreContextVersion: 1,
|
|
138
187
|
scheduledTaskIdentityVersion: 1,
|
|
139
188
|
scheduledTaskCancellationVersion: 1,
|
|
140
189
|
...(state.native?.scheduledTaskProposalSource ? { scheduledTaskProposalVersion: 1 } : {}),
|
|
@@ -142,6 +191,12 @@ async function dispatchRuntimeRequest(state, request, response) {
|
|
|
142
191
|
directoryConcurrencyVersion: 1,
|
|
143
192
|
skillExecutionPolicyVersion: 1,
|
|
144
193
|
parameterInitializationVersion: 1,
|
|
194
|
+
levelEditorVersion: 1,
|
|
195
|
+
levelEditorTaskVersion: 1,
|
|
196
|
+
levelEditorHistoryVersion: 1,
|
|
197
|
+
levelEditorTransactionVersion: 2,
|
|
198
|
+
levelEditorExtensionVersion: 1,
|
|
199
|
+
levelEditorRetryVersion: 1,
|
|
145
200
|
remixReferencesVersion: 1,
|
|
146
201
|
batchSourceFreezeRevision: 2,
|
|
147
202
|
...(state.native?.workspaceUi ? { workspaceUiVersion: 1 } : {}),
|
|
@@ -789,6 +844,44 @@ async function dispatchRuntimeRequest(state, request, response) {
|
|
|
789
844
|
await writeGitRead(state, response, async () => readWorkspaceGitGraph(await workspaceRoot(state, url.searchParams.get('workspaceId') ?? undefined, directoryName)));
|
|
790
845
|
return;
|
|
791
846
|
}
|
|
847
|
+
if (request.method === 'GET' && url.pathname === '/runtime/level-editor') {
|
|
848
|
+
if (writeDestroyed(state, response))
|
|
849
|
+
return;
|
|
850
|
+
if (directoryName === 'root') {
|
|
851
|
+
writeJson(response, 400, { error: 'invalid_workspace_directory' });
|
|
852
|
+
return;
|
|
853
|
+
}
|
|
854
|
+
const workspaceId = url.searchParams.get('workspaceId') ?? undefined;
|
|
855
|
+
const root = await workspaceRoot(state, workspaceId, directoryName), scope = revisionScope(state, workspaceId, directoryName);
|
|
856
|
+
const first = await readWorkspaceLevelEditor(root, state.journal.fileRevisions(scope), { state, workspaceId, directoryName });
|
|
857
|
+
for (const document of Object.values(first.definition?.documents ?? {}))
|
|
858
|
+
await withObservedFile(state, workspaceId, document.path, async () => undefined, directoryName);
|
|
859
|
+
writeJson(response, 200, await readWorkspaceLevelEditor(root, state.journal.fileRevisions(scope), { state, workspaceId, directoryName }));
|
|
860
|
+
return;
|
|
861
|
+
}
|
|
862
|
+
if (request.method === 'GET' && url.pathname === '/runtime/level-editor/tasks') {
|
|
863
|
+
if (writeDestroyed(state, response))
|
|
864
|
+
return;
|
|
865
|
+
if (directoryName === 'root') {
|
|
866
|
+
writeJson(response, 400, { error: 'invalid_workspace_directory' });
|
|
867
|
+
return;
|
|
868
|
+
}
|
|
869
|
+
const workspaceId = url.searchParams.get('workspaceId') ?? undefined;
|
|
870
|
+
writeJson(response, 200, { tasks: await listLevelEditorTasks(state, workspaceId, directoryName, await workspaceRoot(state, workspaceId, directoryName)) });
|
|
871
|
+
return;
|
|
872
|
+
}
|
|
873
|
+
if (request.method === 'GET' && url.pathname === '/runtime/level-editor/requests') {
|
|
874
|
+
if (writeDestroyed(state, response))
|
|
875
|
+
return;
|
|
876
|
+
if (directoryName === 'root') {
|
|
877
|
+
writeJson(response, 400, { error: 'invalid_workspace_directory' });
|
|
878
|
+
return;
|
|
879
|
+
}
|
|
880
|
+
const workspaceId = url.searchParams.get('workspaceId') ?? undefined;
|
|
881
|
+
await workspaceRoot(state, workspaceId, directoryName);
|
|
882
|
+
writeJson(response, 200, { requests: await listLevelEditorRequests(state, workspaceId, directoryName) });
|
|
883
|
+
return;
|
|
884
|
+
}
|
|
792
885
|
if (request.method === 'GET' && url.pathname === '/runtime/parameters') {
|
|
793
886
|
if (writeDestroyed(state, response))
|
|
794
887
|
return;
|
|
@@ -839,7 +932,8 @@ async function dispatchRuntimeRequest(state, request, response) {
|
|
|
839
932
|
directoryName,
|
|
840
933
|
sessionId: url.searchParams.get('sessionId') ?? undefined,
|
|
841
934
|
}));
|
|
842
|
-
|
|
935
|
+
const observed = operation ? await observeLevelEditorTransaction(state, operation) : undefined;
|
|
936
|
+
writeJson(response, observed ? 200 : 404, observed ?? { error: 'operation_not_found' });
|
|
843
937
|
return;
|
|
844
938
|
}
|
|
845
939
|
const operationLog = /^\/runtime\/operations\/([0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12})\/log$/.exec(url.pathname);
|
|
@@ -873,7 +967,7 @@ async function dispatchRuntimeRequest(state, request, response) {
|
|
|
873
967
|
writeJson(response, 404, { error: 'operation_not_found' });
|
|
874
968
|
return;
|
|
875
969
|
}
|
|
876
|
-
writeJson(response, 200, operation);
|
|
970
|
+
writeJson(response, 200, await observeLevelEditorTransaction(state, operation));
|
|
877
971
|
return;
|
|
878
972
|
}
|
|
879
973
|
if (request.method === 'POST' && url.pathname === '/runtime/scheduled-task-proposals/source') {
|
|
@@ -11,7 +11,7 @@ export function buildOperationRecord(state, parsed, payloadHash, leaseFence, exe
|
|
|
11
11
|
...(parsed.gatewayOperationId ? { gatewayOperationId: parsed.gatewayOperationId } : {}),
|
|
12
12
|
...(policy ? { skillExecutionPolicyVersion: policy.skillExecutionPolicyVersion, directoryAccessMode: policy.directoryAccessMode,
|
|
13
13
|
...workspaceComposerSelectionSchema.parse({ context: payload.context, workMode: payload.workMode }) } : {}),
|
|
14
|
-
...(parsed.kind === 'session.turn' && payload.parameterTask !== undefined ? { directoryAccessMode: 'exclusive' } : {}),
|
|
14
|
+
...(parsed.kind === 'session.turn' && (payload.parameterTask !== undefined || payload.levelEditorTask !== undefined) ? { directoryAccessMode: 'exclusive' } : {}),
|
|
15
15
|
environment: state.config.environment,
|
|
16
16
|
userId: state.config.userId,
|
|
17
17
|
clientRequestId: parsed.clientRequestId,
|