@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.
Files changed (39) hide show
  1. package/dist/workspace-runtime/batch/deliver.js +105 -87
  2. package/dist/workspace-runtime/batch/prepare.js +169 -56
  3. package/dist/workspace-runtime/batch/source-capture.js +7 -20
  4. package/dist/workspace-runtime/batch/source-publish.js +2 -4
  5. package/dist/workspace-runtime/codex/app-server.js +19 -1
  6. package/dist/workspace-runtime/codex/delivery-tools.js +144 -0
  7. package/dist/workspace-runtime/codex/tool-response.js +10 -0
  8. package/dist/workspace-runtime/codex/ui-tools.js +22 -5
  9. package/dist/workspace-runtime/previews/diagnostics.js +44 -0
  10. package/dist/workspace-runtime/previews/publish.js +29 -8
  11. package/dist/workspace-runtime/previews/runtime-preview.js +177 -0
  12. package/dist/workspace-runtime/serve.js +162 -4
  13. package/dist/workspace-runtime/server/accept-operation.js +61 -0
  14. package/dist/workspace-runtime/server/asset-transfers.js +63 -0
  15. package/dist/workspace-runtime/server/dispatch.js +328 -44
  16. package/dist/workspace-runtime/server/http.js +193 -137
  17. package/dist/workspace-runtime/server/operation-record.js +33 -0
  18. package/dist/workspace-runtime/server/submit-operation.js +41 -0
  19. package/dist/workspace-runtime/workspaces/append-only-log.js +62 -0
  20. package/dist/workspace-runtime/workspaces/asset-transfer-dependencies.js +81 -0
  21. package/dist/workspace-runtime/workspaces/asset-transfer-publish.js +434 -0
  22. package/dist/workspace-runtime/workspaces/asset-transfer-storage.js +389 -0
  23. package/dist/workspace-runtime/workspaces/delivery-build.js +331 -0
  24. package/dist/workspace-runtime/workspaces/delivery-store.js +168 -0
  25. package/dist/workspace-runtime/workspaces/delivery.js +64 -0
  26. package/dist/workspace-runtime/workspaces/dependencies.js +15 -0
  27. package/dist/workspace-runtime/workspaces/developer-command.js +35 -1
  28. package/dist/workspace-runtime/workspaces/directories.js +9 -4
  29. package/dist/workspace-runtime/workspaces/directory-mutations.js +238 -0
  30. package/dist/workspace-runtime/workspaces/directory-revisions.js +56 -0
  31. package/dist/workspace-runtime/workspaces/disk-snapshot.js +3 -3
  32. package/dist/workspace-runtime/workspaces/json5-edit.js +144 -1
  33. package/dist/workspace-runtime/workspaces/operation-log.js +23 -0
  34. package/dist/workspace-runtime/workspaces/parameter-agent-inputs.js +87 -0
  35. package/dist/workspace-runtime/workspaces/parameter-agent-protocol.js +29 -0
  36. package/dist/workspace-runtime/workspaces/parameter-agent-store.js +58 -0
  37. package/dist/workspace-runtime/workspaces/parameter-agent.js +282 -0
  38. package/dist/workspace-runtime/workspaces/parameters.js +77 -54
  39. 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
- const result = await native(state).startTurn({
391
- sessionId,
392
- threadId: binding.threadId,
393
- text: composer.append(batchContext + context + String(body.text ?? '') + attachmentText, engineeringBatchContext),
394
- images: attachments.filter((file) => ['image/png', 'image/jpeg', 'image/gif', 'image/webp'].includes(file.mimeType)).map((file) => file.path),
395
- clientRequestId: operation.clientRequestId,
396
- modelConfigId: selected.data ?? binding.modelConfigId,
397
- reasoningEffort: requestedEffort,
398
- permissionMode: requestedMode,
399
- workMode: selection.data.workMode ?? 'execute',
400
- skillsLockHash: composer.skillsLockHash,
401
- uiSnapshot: uiSnapshot.data,
402
- simulateLostReceipt: body.simulateLostReceipt === true,
403
- persistReservation: async ({ nativeRequestId, modelConfigId, reasoningEffort, permissionMode }) => {
404
- const selectedEffort = reasoningEffort ?? requestedEffort;
405
- const selectedMode = permissionMode ?? requestedMode;
406
- await persist(state, scopeKey, {
407
- ...operation,
408
- nativeRequestId,
409
- nativeThreadId: binding.threadId,
410
- modelConfigId,
411
- reasoningEffort: selectedEffort,
412
- permissionMode: selectedMode,
413
- workMode: selection.data.workMode ?? 'execute',
414
- context: composer.context,
415
- userText: selection.data.workMode !== undefined || composer.context !== undefined
416
- ? String(body.userText ?? body.text ?? '') : undefined,
417
- updatedAt: new Date().toISOString(),
418
- });
419
- await state.journal.putSession({ ...binding, modelConfigId, reasoningEffort: selectedEffort, permissionMode: selectedMode, turnStatus: 'starting' });
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
- return state.journal.getOperation(scopeKey) ?? latest;
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
- fieldPath: parsed.data.fieldPath,
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
- return state.journal.putOperation(scopeKey, operation);
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)