@playdrop/playdrop-cli 0.17.1 → 0.17.4

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.
@@ -150,6 +150,7 @@ const provider_telemetry_1 = require("./worker/provider-telemetry");
150
150
  const archive_staging_1 = require("./worker/archive-staging");
151
151
  const game_server_infra_1 = require("./worker/game-server-infra");
152
152
  const game_source_git_1 = require("./worker/game-source-git");
153
+ const game_source_commands_1 = require("./worker/game-source-commands");
153
154
  const generation_1 = require("./worker/generation");
154
155
  const e2e_fixtures_1 = require("./worker/e2e-fixtures");
155
156
  var archive_staging_2 = require("./worker/archive-staging");
@@ -1241,9 +1242,6 @@ function workerTaskUploadFailurePath(workspaceDir) {
1241
1242
  function workerTaskMaterialsPath(workspaceDir) {
1242
1243
  return node_path_1.default.join(workspaceDir, ".playdrop", "task-materials.json");
1243
1244
  }
1244
- function workerGameSourceReadinessPath(workspaceDir) {
1245
- return node_path_1.default.join(workspaceDir, ".playdrop", "game-source-ready.json");
1246
- }
1247
1245
  function workerGameSourceCompletionPath(workspaceDir) {
1248
1246
  return node_path_1.default.join(workspaceDir, ".playdrop", "game-source-completion.json");
1249
1247
  }
@@ -5636,6 +5634,11 @@ async function startWorker(options = {}) {
5636
5634
  let telemetryReported = false;
5637
5635
  let gameServerDevLease = null;
5638
5636
  let gameSourceWorkspace = null;
5637
+ let gameSourceCommandServer = null;
5638
+ let gameSourcePublication = null;
5639
+ let previousGameSourceCommitSha;
5640
+ let preparedGameSourceProposal = null;
5641
+ let completedGameSourceGeneration = null;
5639
5642
  let transcriptSpoolPath = null;
5640
5643
  let transcriptSpoolSequence = 0;
5641
5644
  let heartbeatTransientFailures = 0;
@@ -5643,6 +5646,8 @@ async function startWorker(options = {}) {
5643
5646
  let automaticContinuationAttempted = false;
5644
5647
  let workerFailureStage = "setup";
5645
5648
  const sendTaskHeartbeat = async () => {
5649
+ if (completedGameSourceGeneration)
5650
+ return;
5646
5651
  try {
5647
5652
  const heartbeat = await client.workerHeartbeatAgentTask(task.id, { workerKey, leaseToken });
5648
5653
  heartbeatTransientFailures = 0;
@@ -5708,6 +5713,8 @@ async function startWorker(options = {}) {
5708
5713
  startedAt: agentStartedAt ?? completedAt,
5709
5714
  completedAt,
5710
5715
  ...(terminalReason ? { terminalReason } : {}),
5716
+ }).catch((error) => {
5717
+ console.error(`Agent task ${task.id} telemetry could not be recorded: ${error instanceof Error ? error.message : String(error)}`);
5711
5718
  });
5712
5719
  telemetryReported = true;
5713
5720
  };
@@ -5803,6 +5810,255 @@ async function startWorker(options = {}) {
5803
5810
  });
5804
5811
  await reportTelemetry("FAILED", undefined, terminalReason);
5805
5812
  };
5813
+ // The supervisor keeps the lease, but both commands wait for the real
5814
+ // operation while the agent can still read errors, repair, and retry.
5815
+ const publishGameSourceForAgent = async () => {
5816
+ if (gameSourcePublication)
5817
+ return gameSourcePublication.upload;
5818
+ if (fenced || shuttingDown)
5819
+ throw new Error("agent_task_lease_invalid");
5820
+ if (!gameSourceWorkspace || !workspaceDir || !agentWorkspaceDir) {
5821
+ throw new Error("game_source_workspace_missing");
5822
+ }
5823
+ const currentTask = await fetchTaskDetail(client, target, task.id, workerKey);
5824
+ if (currentTask.task.status !== "RUNNING")
5825
+ throw new Error("agent_task_not_running");
5826
+ if (task.kind === "STATIC_GAME" && !(0, node_fs_1.existsSync)(node_path_1.default.join(agentWorkspaceDir, "catalogue.json"))) {
5827
+ const primarySurface = resolveWorkerInstrumentSurface(taskContext);
5828
+ if (!primarySurface)
5829
+ throw new Error("static_game_artifact_primary_surface_missing");
5830
+ if (!envConfig.cdnBase)
5831
+ throw new Error("static_game_publish_cdn_base_missing");
5832
+ const preparedStatic = await (0, staticHtml_1.prepareStaticHtmlProject)({
5833
+ projectDir: agentWorkspaceDir,
5834
+ taskId: task.id,
5835
+ appName: currentTask.task.claimedAppName ?? undefined,
5836
+ primarySurface,
5837
+ cdnBase: envConfig.cdnBase,
5838
+ });
5839
+ try {
5840
+ const sourceTask = preparedStatic.sourceTask;
5841
+ await (0, promises_1.writeFile)(node_path_1.default.join(agentWorkspaceDir, "catalogue.json"), `${JSON.stringify({
5842
+ apps: [{
5843
+ name: sourceTask.name,
5844
+ version: sourceTask.version,
5845
+ displayName: sourceTask.displayName,
5846
+ subtitle: sourceTask.subtitle,
5847
+ description: sourceTask.description,
5848
+ type: "GAME",
5849
+ authMode: "NONE",
5850
+ controllerMode: "UNSUPPORTED",
5851
+ previewable: true,
5852
+ file: "index.html",
5853
+ primarySurface,
5854
+ surfaceTargets: {
5855
+ desktop: sourceTask.surfaceTargets.includes("DESKTOP"),
5856
+ mobileLandscape: sourceTask.surfaceTargets.includes("MOBILE_LANDSCAPE"),
5857
+ mobilePortrait: sourceTask.surfaceTargets.includes("MOBILE_PORTRAIT"),
5858
+ },
5859
+ releaseNotes: sourceTask.releaseNotes,
5860
+ hostingMode: "HOSTED",
5861
+ license: "CLOSED",
5862
+ }],
5863
+ }, null, 2)}\n`, "utf8");
5864
+ }
5865
+ finally {
5866
+ await preparedStatic.cleanup();
5867
+ }
5868
+ }
5869
+ const outputProjectDir = discoverWorkerProjectRoot(agentWorkspaceDir);
5870
+ if (node_path_1.default.resolve(outputProjectDir) !== node_path_1.default.resolve(gameSourceWorkspace.projectDir)) {
5871
+ throw new Error("game_source_output_must_be_repository_root");
5872
+ }
5873
+ workerFailureStage = "game_source_reconcile";
5874
+ const reconciled = await (0, game_source_git_1.reconcileGameSourceWorkspace)({
5875
+ workspace: gameSourceWorkspace,
5876
+ taskId: task.id,
5877
+ summary: `Prepared ${taskContext.outputAppName ?? "game"} for publication`,
5878
+ previousCommitSha: previousGameSourceCommitSha,
5879
+ });
5880
+ previousGameSourceCommitSha = reconciled.commitSha;
5881
+ const taskClient = (0, apiClient_1.createCliApiClient)({
5882
+ baseUrl: envConfig.apiBase,
5883
+ tokenProvider: ctx.getToken,
5884
+ onBehalfCreatorUsername: taskContext.creatorUsername,
5885
+ agentTaskToken: assignment.task.token,
5886
+ agentTaskId: task.id,
5887
+ agentTaskAttempt: taskContext.attempt,
5888
+ });
5889
+ let published;
5890
+ let finalizedGameSource = null;
5891
+ let preparedGameSource = null;
5892
+ const prepareGitTransition = async (input) => {
5893
+ const versionParts = input.version.split(".").map((part) => Number(part));
5894
+ if (versionParts.length !== 3 ||
5895
+ versionParts.some((part) => !Number.isSafeInteger(part) || part < 0)) {
5896
+ throw new Error("game_source_version_invalid");
5897
+ }
5898
+ // A finalize response can be lost after the version is saved. An
5899
+ // unchanged retry must keep that exact source transition, not a
5900
+ // new timestamped commit or bundle. This cache is attempt-local.
5901
+ if (preparedGameSourceProposal?.appId === input.appId &&
5902
+ preparedGameSourceProposal.version === input.version &&
5903
+ preparedGameSourceProposal.finalized.commitSha === reconciled.commitSha) {
5904
+ finalizedGameSource = preparedGameSourceProposal.finalized;
5905
+ }
5906
+ else {
5907
+ finalizedGameSource = await (0, game_source_git_1.finalizeGameSourceRepository)({
5908
+ reconciled,
5909
+ appVersionId: 0,
5910
+ version: { major: versionParts[0], minor: versionParts[1], patch: versionParts[2] },
5911
+ });
5912
+ preparedGameSourceProposal = {
5913
+ appId: input.appId,
5914
+ version: input.version,
5915
+ finalized: finalizedGameSource,
5916
+ };
5917
+ }
5918
+ // Recheck ownership and generation for this upload session, even
5919
+ // when the immutable bundle is reused after an aborted session.
5920
+ preparedGameSource = await (0, game_source_git_1.uploadFinalizedGameSource)({
5921
+ client,
5922
+ taskId: task.id,
5923
+ attempt: taskContext.attempt,
5924
+ workerKey,
5925
+ leaseToken,
5926
+ appId: input.appId,
5927
+ appUploadSessionId: input.sessionId,
5928
+ source: finalizedGameSource,
5929
+ });
5930
+ return {
5931
+ commitSha: preparedGameSource.commitSha,
5932
+ headSha: preparedGameSource.headSha,
5933
+ baseGeneration: preparedGameSource.baseGeneration,
5934
+ bundleStorageKey: preparedGameSource.bundleStorageKey,
5935
+ bundleSha256: preparedGameSource.bundleSha256,
5936
+ bundleSizeBytes: preparedGameSource.bundleBytes,
5937
+ };
5938
+ };
5939
+ workerFailureStage = "game_publish";
5940
+ if (task.kind === "STATIC_GAME") {
5941
+ const primarySurface = resolveWorkerInstrumentSurface(taskContext);
5942
+ if (!primarySurface)
5943
+ throw new Error("static_game_artifact_primary_surface_missing");
5944
+ if (!envConfig.cdnBase)
5945
+ throw new Error("static_game_publish_cdn_base_missing");
5946
+ const result = await (0, upload_1.publishStaticHtmlProject)({
5947
+ client: taskClient,
5948
+ taskId: task.id,
5949
+ taskToken: assignment.task.token,
5950
+ projectDir: agentWorkspaceDir,
5951
+ creatorUsername: taskContext.creatorUsername,
5952
+ primarySurface,
5953
+ apiBase: envConfig.apiBase,
5954
+ webBase: envConfig.webBase ?? null,
5955
+ cdnBase: envConfig.cdnBase,
5956
+ claimedAppName: currentTask.task.claimedAppName,
5957
+ devRouterPort: devPort,
5958
+ token: ctx.getToken(),
5959
+ user: me.user,
5960
+ prepareGitTransition,
5961
+ });
5962
+ published = result;
5963
+ }
5964
+ else {
5965
+ const fixtureKind = (0, e2e_fixtures_1.detectPlaydropE2EFixture)(assignment.request.prompt);
5966
+ const result = await (0, upload_1.publishWorkerAppProject)({
5967
+ client: taskClient,
5968
+ taskId: task.id,
5969
+ taskToken: assignment.task.token,
5970
+ kind: task.kind,
5971
+ executionTarget: task.executionTarget,
5972
+ expectedAppName: taskContext.outputAppName ?? undefined,
5973
+ expectedDisplayName: taskContext.outputDisplayName,
5974
+ expectedSubtitle: taskContext.outputSubtitle,
5975
+ expectedPrimarySurface: taskContext.outputPrimarySurface,
5976
+ remixSourceRef: taskContext.remixSourceRef ?? null,
5977
+ projectDir: agentWorkspaceDir,
5978
+ creatorUsername: taskContext.creatorUsername,
5979
+ apiBase: envConfig.apiBase,
5980
+ webBase: envConfig.webBase ?? null,
5981
+ token: ctx.getToken(),
5982
+ user: me.user,
5983
+ devRouterPort: devPort,
5984
+ deterministicE2EFixture: fixtureKind === task.kind,
5985
+ prepareGitTransition,
5986
+ });
5987
+ published = result;
5988
+ }
5989
+ if (!finalizedGameSource || !preparedGameSource) {
5990
+ throw new Error("game_source_transition_not_prepared");
5991
+ }
5992
+ const finalized = finalizedGameSource;
5993
+ const gameSource = preparedGameSource;
5994
+ if (!published.versionNodeId)
5995
+ throw new Error("game_source_version_node_missing");
5996
+ const upload = {
5997
+ taskId: task.id,
5998
+ appId: published.appId,
5999
+ appVersionId: published.appVersionId,
6000
+ appName: published.appName,
6001
+ version: published.version,
6002
+ versionNodeId: published.versionNodeId,
6003
+ creatorUsername: taskContext.creatorUsername,
6004
+ };
6005
+ gameSourcePublication = { upload, source: gameSource, finalized };
6006
+ await (0, promises_1.writeFile)(workerTaskUploadResultPath(workspaceDir), JSON.stringify(upload, null, 2), "utf8");
6007
+ await (0, promises_1.rm)(workerTaskUploadFailurePath(workspaceDir), { force: true });
6008
+ for (const warning of published.warnings)
6009
+ console.error(`Upload warning: ${warning}`);
6010
+ return upload;
6011
+ };
6012
+ const completeGameSourceForAgent = async (intent) => {
6013
+ if (completedGameSourceGeneration)
6014
+ return;
6015
+ if (fenced || shuttingDown)
6016
+ throw new Error("agent_task_lease_invalid");
6017
+ if (!gameSourcePublication)
6018
+ throw new Error("task_done_missing_upload_result: Run playdrop task upload successfully first.");
6019
+ if (intent.taskId !== task.id || intent.attempt !== taskContext.attempt ||
6020
+ typeof intent.summary !== "string" || !intent.summary.trim() ||
6021
+ intent.summary.length > 200 || /[\r\n]/.test(intent.summary)) {
6022
+ throw new Error("game_source_completion_intent_invalid");
6023
+ }
6024
+ const { upload: published, source: gameSource, finalized } = gameSourcePublication;
6025
+ workerFailureStage = "task_complete";
6026
+ const completion = await completeWorkerAgentTaskWithRetry({
6027
+ client,
6028
+ taskId: task.id,
6029
+ body: {
6030
+ workerKey,
6031
+ leaseToken,
6032
+ appId: published.appId,
6033
+ appVersionId: published.appVersionId,
6034
+ result: {
6035
+ appId: published.appId,
6036
+ appVersionId: published.appVersionId,
6037
+ appName: published.appName,
6038
+ version: published.version,
6039
+ versionNodeId: published.versionNodeId,
6040
+ completedBy: "worker_game_source_git",
6041
+ },
6042
+ summary: intent.summary,
6043
+ ...(intent.nextSteps ? { nextSteps: intent.nextSteps } : {}),
6044
+ ...(intent.platformFeedback ? { platformFeedback: intent.platformFeedback } : {}),
6045
+ ...(agentResult ? { tokensUsed: agentResult.tokensUsed } : {}),
6046
+ gameSource,
6047
+ },
6048
+ });
6049
+ if (!completion.gameSource)
6050
+ throw new Error("game_source_completion_metadata_missing");
6051
+ completedGameSourceGeneration = completion.gameSource.repoGeneration;
6052
+ (0, game_source_git_1.emitGameSourceWorkerEvent)("playdrop.game_source.completed", {
6053
+ taskId: task.id,
6054
+ appId: published.appId,
6055
+ repoGeneration: completion.gameSource.repoGeneration,
6056
+ bundleBytes: finalized.bundle.length,
6057
+ materialCount: finalized.materials.length,
6058
+ outcome: "DONE",
6059
+ });
6060
+ retainWorkspace = false;
6061
+ };
5806
6062
  try {
5807
6063
  await sendTaskHeartbeat();
5808
6064
  if (sessionExpired) {
@@ -5896,13 +6152,18 @@ async function startWorker(options = {}) {
5896
6152
  devPort,
5897
6153
  gameSourceGit: Boolean(assignment.gameSource),
5898
6154
  });
5899
- const handleEventDrainFailure = (error) => {
6155
+ const handleEventDrainFailure = async (error) => {
6156
+ if (completedGameSourceGeneration)
6157
+ return;
5900
6158
  const classification = classifyWorkerEventDrainError(error);
5901
6159
  if (classification === "session_expired") {
5902
6160
  handleSessionExpiry();
5903
6161
  return;
5904
6162
  }
5905
6163
  if (classification === "lease_invalid") {
6164
+ const current = await fetchTaskDetail(client, target, task.id, workerKey).catch(() => null);
6165
+ if (current?.task.status === "DONE")
6166
+ return;
5906
6167
  fenced = true;
5907
6168
  activeTerminators.get(task.id)?.();
5908
6169
  return;
@@ -5910,7 +6171,10 @@ async function startWorker(options = {}) {
5910
6171
  console.error(`Agent task event drain failed: ${error instanceof Error ? error.message : String(error)}`);
5911
6172
  };
5912
6173
  const queueEventDrain = () => {
5913
- eventDrainPromise = eventDrainPromise.then(() => drainWorkerEventQueue({ eventDir, client, taskId: task.id, workerKey, leaseToken }), () => drainWorkerEventQueue({ eventDir, client, taskId: task.id, workerKey, leaseToken }));
6174
+ const drain = () => completedGameSourceGeneration
6175
+ ? Promise.resolve()
6176
+ : drainWorkerEventQueue({ eventDir, client, taskId: task.id, workerKey, leaseToken });
6177
+ eventDrainPromise = eventDrainPromise.then(drain, drain);
5914
6178
  return eventDrainPromise;
5915
6179
  };
5916
6180
  const appendObservedSkillPathsFromTranscript = (chunks) => {
@@ -6041,7 +6305,7 @@ async function startWorker(options = {}) {
6041
6305
  }
6042
6306
  eventDrainTimer = setInterval(() => {
6043
6307
  queueEventDrain().catch((error) => {
6044
- handleEventDrainFailure(error);
6308
+ return handleEventDrainFailure(error);
6045
6309
  });
6046
6310
  }, 1000);
6047
6311
  const gameCreationTask = usesCreatorRecovery(task.kind);
@@ -6078,7 +6342,7 @@ async function startWorker(options = {}) {
6078
6342
  try {
6079
6343
  enqueueWorkerEvent(eventDir, initialProgress);
6080
6344
  queueEventDrain().catch((error) => {
6081
- handleEventDrainFailure(error);
6345
+ return handleEventDrainFailure(error);
6082
6346
  });
6083
6347
  }
6084
6348
  catch (error) {
@@ -6223,6 +6487,31 @@ async function startWorker(options = {}) {
6223
6487
  message: "Resumed the retained game workspace with a focused creation recovery window.",
6224
6488
  });
6225
6489
  }
6490
+ if (assignment.gameSource && task.kind !== "STATIC_GAME") {
6491
+ gameSourceCommandServer = await (0, game_source_commands_1.startWorkerGameSourceCommandServer)({
6492
+ workspaceDir,
6493
+ taskId: task.id,
6494
+ attempt: taskContext.attempt,
6495
+ onCommand: async (request) => {
6496
+ try {
6497
+ if (request.command === "upload")
6498
+ return await publishGameSourceForAgent();
6499
+ if (!request.payload || typeof request.payload !== "object") {
6500
+ throw new Error("game_source_completion_intent_invalid");
6501
+ }
6502
+ await completeGameSourceForAgent(request.payload);
6503
+ return { completed: true };
6504
+ }
6505
+ catch (error) {
6506
+ await recordTaskUploadFailure({ workspaceDir: workspaceDir, taskContext, error }).catch((recordError) => console.error(`Could not record task command failure: ${recordError}`));
6507
+ throw error;
6508
+ }
6509
+ finally {
6510
+ workerFailureStage = "agent";
6511
+ }
6512
+ },
6513
+ });
6514
+ }
6226
6515
  workerFailureStage = "agent";
6227
6516
  agentResult = await runAssignedAgent({
6228
6517
  prompt,
@@ -6314,10 +6603,12 @@ async function startWorker(options = {}) {
6314
6603
  }
6315
6604
  }
6316
6605
  }
6606
+ await gameSourceCommandServer?.close();
6607
+ gameSourceCommandServer = null;
6317
6608
  resolvedRuntimeModel = agentResult.providerResolvedModel?.trim() || resolvedRuntimeModel;
6318
6609
  agentCompletedAt = new Date();
6319
6610
  await queueEventDrain().catch((error) => {
6320
- handleEventDrainFailure(error);
6611
+ return handleEventDrainFailure(error);
6321
6612
  });
6322
6613
  void archiveWorkerTranscriptBestEffort({
6323
6614
  client,
@@ -6461,215 +6752,16 @@ async function startWorker(options = {}) {
6461
6752
  await failMissingAgentOutcome(agentRunResult);
6462
6753
  }
6463
6754
  else if (assignment.gameSource) {
6464
- if (!gameSourceWorkspace || !workspaceDir || !agentWorkspaceDir) {
6465
- throw new Error("game_source_workspace_missing");
6466
- }
6467
- const intent = task.kind === "STATIC_GAME"
6468
- ? {
6469
- taskId: task.id,
6470
- attempt: taskContext.attempt,
6471
- summary: "Created and privately hosted a standalone static web game.",
6472
- }
6473
- : readWorkerGameSourceCompletion(workspaceDir);
6474
- if (!intent || intent.taskId !== task.id || intent.attempt !== taskContext.attempt) {
6475
- throw new Error("agent_exited_without_task_done");
6755
+ if (task.kind !== "STATIC_GAME") {
6756
+ throw new Error("agent_exited_without_successful_task_done");
6476
6757
  }
6477
- if (task.kind !== "STATIC_GAME" && !(0, node_fs_1.existsSync)(workerGameSourceReadinessPath(workspaceDir))) {
6478
- throw new Error("agent_exited_without_task_upload");
6479
- }
6480
- if (task.kind === "STATIC_GAME" && !(0, node_fs_1.existsSync)(node_path_1.default.join(agentWorkspaceDir, "catalogue.json"))) {
6481
- const primarySurface = resolveWorkerInstrumentSurface(taskContext);
6482
- if (!primarySurface)
6483
- throw new Error("static_game_artifact_primary_surface_missing");
6484
- if (!envConfig.cdnBase)
6485
- throw new Error("static_game_publish_cdn_base_missing");
6486
- const preparedStatic = await (0, staticHtml_1.prepareStaticHtmlProject)({
6487
- projectDir: agentWorkspaceDir,
6488
- taskId: task.id,
6489
- appName: refreshed.task.claimedAppName ?? undefined,
6490
- primarySurface,
6491
- cdnBase: envConfig.cdnBase,
6492
- });
6493
- try {
6494
- const sourceTask = preparedStatic.sourceTask;
6495
- await (0, promises_1.writeFile)(node_path_1.default.join(agentWorkspaceDir, "catalogue.json"), `${JSON.stringify({
6496
- apps: [{
6497
- name: sourceTask.name,
6498
- version: sourceTask.version,
6499
- displayName: sourceTask.displayName,
6500
- subtitle: sourceTask.subtitle,
6501
- description: sourceTask.description,
6502
- type: "GAME",
6503
- authMode: "NONE",
6504
- controllerMode: "UNSUPPORTED",
6505
- previewable: true,
6506
- file: "index.html",
6507
- primarySurface,
6508
- surfaceTargets: {
6509
- desktop: sourceTask.surfaceTargets.includes("DESKTOP"),
6510
- mobileLandscape: sourceTask.surfaceTargets.includes("MOBILE_LANDSCAPE"),
6511
- mobilePortrait: sourceTask.surfaceTargets.includes("MOBILE_PORTRAIT"),
6512
- },
6513
- releaseNotes: sourceTask.releaseNotes,
6514
- hostingMode: "HOSTED",
6515
- license: "CLOSED",
6516
- }],
6517
- }, null, 2)}\n`, "utf8");
6518
- }
6519
- finally {
6520
- await preparedStatic.cleanup();
6521
- }
6522
- }
6523
- const outputProjectDir = discoverWorkerProjectRoot(agentWorkspaceDir);
6524
- if (node_path_1.default.resolve(outputProjectDir) !== node_path_1.default.resolve(gameSourceWorkspace.projectDir)) {
6525
- throw new Error("game_source_output_must_be_repository_root");
6526
- }
6527
- workerFailureStage = "game_source_reconcile";
6528
- const reconciled = await (0, game_source_git_1.reconcileGameSourceWorkspace)({
6529
- workspace: gameSourceWorkspace,
6758
+ await publishGameSourceForAgent();
6759
+ await completeGameSourceForAgent({
6530
6760
  taskId: task.id,
6531
- summary: intent.summary,
6532
- });
6533
- const taskClient = (0, apiClient_1.createCliApiClient)({
6534
- baseUrl: envConfig.apiBase,
6535
- tokenProvider: ctx.getToken,
6536
- onBehalfCreatorUsername: taskContext.creatorUsername,
6537
- agentTaskToken: assignment.task.token,
6538
- agentTaskId: task.id,
6539
- agentTaskAttempt: taskContext.attempt,
6540
- });
6541
- let published;
6542
- let finalizedGameSource = null;
6543
- let preparedGameSource = null;
6544
- const prepareGitTransition = async (input) => {
6545
- const versionParts = input.version.split(".").map((part) => Number(part));
6546
- if (versionParts.length !== 3 ||
6547
- versionParts.some((part) => !Number.isSafeInteger(part) || part < 0)) {
6548
- throw new Error("game_source_version_invalid");
6549
- }
6550
- finalizedGameSource = await (0, game_source_git_1.finalizeGameSourceRepository)({
6551
- reconciled,
6552
- appVersionId: 0,
6553
- version: { major: versionParts[0], minor: versionParts[1], patch: versionParts[2] },
6554
- });
6555
- preparedGameSource = await (0, game_source_git_1.uploadFinalizedGameSource)({
6556
- client,
6557
- taskId: task.id,
6558
- attempt: taskContext.attempt,
6559
- workerKey,
6560
- leaseToken,
6561
- appId: input.appId,
6562
- appUploadSessionId: input.sessionId,
6563
- source: finalizedGameSource,
6564
- });
6565
- return {
6566
- commitSha: preparedGameSource.commitSha,
6567
- headSha: preparedGameSource.headSha,
6568
- baseGeneration: preparedGameSource.baseGeneration,
6569
- bundleStorageKey: preparedGameSource.bundleStorageKey,
6570
- bundleSha256: preparedGameSource.bundleSha256,
6571
- bundleSizeBytes: preparedGameSource.bundleBytes,
6572
- };
6573
- };
6574
- workerFailureStage = "game_publish";
6575
- if (task.kind === "STATIC_GAME") {
6576
- const primarySurface = resolveWorkerInstrumentSurface(taskContext);
6577
- if (!primarySurface)
6578
- throw new Error("static_game_artifact_primary_surface_missing");
6579
- if (!envConfig.cdnBase)
6580
- throw new Error("static_game_publish_cdn_base_missing");
6581
- const result = await (0, upload_1.publishStaticHtmlProject)({
6582
- client: taskClient,
6583
- taskId: task.id,
6584
- taskToken: assignment.task.token,
6585
- projectDir: agentWorkspaceDir,
6586
- creatorUsername: taskContext.creatorUsername,
6587
- primarySurface,
6588
- apiBase: envConfig.apiBase,
6589
- webBase: envConfig.webBase ?? null,
6590
- cdnBase: envConfig.cdnBase,
6591
- claimedAppName: refreshed.task.claimedAppName,
6592
- devRouterPort: devPort,
6593
- token: ctx.getToken(),
6594
- user: me.user,
6595
- prepareGitTransition,
6596
- });
6597
- published = result;
6598
- }
6599
- else {
6600
- const fixtureKind = (0, e2e_fixtures_1.detectPlaydropE2EFixture)(assignment.request.prompt);
6601
- const result = await (0, upload_1.publishWorkerAppProject)({
6602
- client: taskClient,
6603
- taskId: task.id,
6604
- taskToken: assignment.task.token,
6605
- kind: task.kind,
6606
- executionTarget: task.executionTarget,
6607
- expectedAppName: taskContext.outputAppName ?? undefined,
6608
- expectedDisplayName: taskContext.outputDisplayName,
6609
- expectedSubtitle: taskContext.outputSubtitle,
6610
- expectedPrimarySurface: taskContext.outputPrimarySurface,
6611
- remixSourceRef: taskContext.remixSourceRef ?? null,
6612
- projectDir: agentWorkspaceDir,
6613
- creatorUsername: taskContext.creatorUsername,
6614
- apiBase: envConfig.apiBase,
6615
- webBase: envConfig.webBase ?? null,
6616
- token: ctx.getToken(),
6617
- user: me.user,
6618
- deterministicE2EFixture: fixtureKind === task.kind,
6619
- prepareGitTransition,
6620
- });
6621
- published = result;
6622
- }
6623
- if (!finalizedGameSource || !preparedGameSource) {
6624
- throw new Error("game_source_transition_not_prepared");
6625
- }
6626
- const finalized = finalizedGameSource;
6627
- const gameSource = preparedGameSource;
6628
- workerFailureStage = "task_complete";
6629
- const completion = await completeWorkerAgentTaskWithRetry({
6630
- client,
6631
- taskId: task.id,
6632
- body: {
6633
- workerKey,
6634
- leaseToken,
6635
- appId: published.appId,
6636
- appVersionId: published.appVersionId,
6637
- result: {
6638
- appId: published.appId,
6639
- appVersionId: published.appVersionId,
6640
- appName: published.appName,
6641
- version: published.version,
6642
- versionNodeId: published.versionNodeId,
6643
- completedBy: "worker_game_source_git",
6644
- },
6645
- summary: intent.summary,
6646
- ...(intent.nextSteps ? { nextSteps: intent.nextSteps } : {}),
6647
- ...(intent.platformFeedback ? { platformFeedback: intent.platformFeedback } : {}),
6648
- tokensUsed: agentResult.tokensUsed,
6649
- gameSource,
6650
- },
6651
- });
6652
- if (!completion.gameSource)
6653
- throw new Error("game_source_completion_metadata_missing");
6654
- (0, game_source_git_1.emitGameSourceWorkerEvent)("playdrop.game_source.completed", {
6655
- taskId: task.id,
6656
- appId: published.appId,
6657
- repoGeneration: completion.gameSource.repoGeneration,
6658
- bundleBytes: finalized.bundle.length,
6659
- materialCount: finalized.materials.length,
6660
- outcome: "DONE",
6761
+ attempt: taskContext.attempt,
6762
+ summary: "Created and privately hosted a standalone static web game.",
6661
6763
  });
6662
6764
  await reportTelemetry("DONE");
6663
- await (0, game_source_git_1.cleanupCompletedGameSourceWorkspace)({
6664
- workspace: gameSourceWorkspace,
6665
- workerHomeDir: resolveWorkerHomeDir(),
6666
- appId: published.appId,
6667
- repoGeneration: completion.gameSource.repoGeneration,
6668
- headSha: finalized.commitSha,
6669
- versions: finalized.versions,
6670
- });
6671
- retainWorkspace = false;
6672
- console.log(`Agent task ${task.id} reconciled, published, and completed with canonical Git source.`);
6673
6765
  }
6674
6766
  else if (task.kind === "STATIC_GAME") {
6675
6767
  await reportTelemetry("DONE");
@@ -6842,6 +6934,8 @@ async function startWorker(options = {}) {
6842
6934
  }
6843
6935
  catch (error) {
6844
6936
  const message = error instanceof Error ? error.message : String(error);
6937
+ await gameSourceCommandServer?.close();
6938
+ gameSourceCommandServer = null;
6845
6939
  if (gameSourceWorkspace) {
6846
6940
  await (0, game_source_git_1.checkpointFailedGameSourceWorkspace)({ workspace: gameSourceWorkspace, taskId: task.id }).catch((checkpointError) => {
6847
6941
  console.error(`Failed Git workspace checkpoint did not complete: ${checkpointError instanceof Error ? checkpointError.message : String(checkpointError)}`);
@@ -6899,6 +6993,7 @@ async function startWorker(options = {}) {
6899
6993
  }
6900
6994
  }
6901
6995
  finally {
6996
+ await gameSourceCommandServer?.close();
6902
6997
  if (heartbeatTimer) {
6903
6998
  clearInterval(heartbeatTimer);
6904
6999
  }
@@ -6907,6 +7002,20 @@ async function startWorker(options = {}) {
6907
7002
  }
6908
7003
  activeTerminators.delete(task.id);
6909
7004
  await gameServerDevLease?.cleanup();
7005
+ if (completedGameSourceGeneration && gameSourcePublication && gameSourceWorkspace) {
7006
+ const publication = gameSourcePublication;
7007
+ await (0, game_source_git_1.cleanupCompletedGameSourceWorkspace)({
7008
+ workspace: gameSourceWorkspace,
7009
+ workerHomeDir: resolveWorkerHomeDir(),
7010
+ appId: publication.upload.appId,
7011
+ repoGeneration: completedGameSourceGeneration,
7012
+ headSha: publication.finalized.commitSha,
7013
+ versions: publication.finalized.versions,
7014
+ }).catch((cleanupError) => {
7015
+ retainWorkspace = true;
7016
+ console.error(`Completed game source cache cleanup failed: ${cleanupError}`);
7017
+ });
7018
+ }
6910
7019
  if (workspaceDir) {
6911
7020
  await removeAgentRunCredentials(workspaceDir);
6912
7021
  }
@@ -7423,37 +7532,20 @@ async function uploadTask(options = {}) {
7423
7532
  return;
7424
7533
  }
7425
7534
  if (taskContext.gameSourceGit) {
7426
- const ctx = await resolveTaskCommandContext("task upload", options.env, taskContext);
7427
- if (!ctx)
7428
- return;
7429
- const projectDir = discoverWorkerProjectRoot(node_process_1.default.cwd());
7430
- const result = await (0, upload_1.preflightWorkerAppProject)({
7431
- client: ctx.client,
7432
- taskId: taskContext.taskId,
7433
- taskToken: taskContext.taskToken,
7434
- kind: uploadKind,
7435
- executionTarget: taskContext.target,
7436
- expectedAppName: taskContext.outputAppName ?? undefined,
7437
- expectedDisplayName: taskContext.outputDisplayName,
7438
- expectedSubtitle: taskContext.outputSubtitle,
7439
- expectedPrimarySurface: taskContext.outputPrimarySurface,
7440
- remixSourceRef: taskContext.remixSourceRef ?? null,
7441
- projectDir,
7442
- creatorUsername: taskContext.creatorUsername,
7443
- apiBase: ctx.envConfig.apiBase,
7444
- webBase: ctx.envConfig.webBase ?? null,
7445
- token: ctx.token,
7446
- user: ctx.user,
7535
+ await withTaskUploadLock(workspaceDir, async () => {
7536
+ const ctx = await resolveTaskCommandContext("task upload", options.env, taskContext);
7537
+ if (!ctx)
7538
+ return;
7539
+ await (0, game_source_commands_1.requestWorkerGameSourceCommand)({
7540
+ workspaceDir,
7541
+ taskId: taskContext.taskId,
7542
+ attempt: taskContext.attempt,
7543
+ command: "upload",
7544
+ });
7545
+ const result = readTaskUploadResultFile(workspaceDir);
7546
+ assertTaskUploadResultMatchesContext({ taskContext, uploadResult: result });
7547
+ (0, output_1.printSuccess)(`Uploaded ${result.creatorUsername}/${result.appName} version ${result.version}. Run "playdrop task done" to close the task.`);
7447
7548
  });
7448
- await (0, promises_1.writeFile)(workerGameSourceReadinessPath(workspaceDir), JSON.stringify({
7449
- taskId: taskContext.taskId,
7450
- attempt: taskContext.attempt,
7451
- projectDir,
7452
- readyAt: new Date().toISOString(),
7453
- }, null, 2));
7454
- for (const warning of result.warnings)
7455
- console.error(`Preflight warning: ${warning}`);
7456
- (0, output_1.printSuccess)("Task source is ready. Run \"playdrop task done\" when the work is complete.");
7457
7549
  return;
7458
7550
  }
7459
7551
  if (taskContext.kind === "STATIC_GAME") {
@@ -7494,6 +7586,7 @@ async function uploadTask(options = {}) {
7494
7586
  webBase: ctx.envConfig.webBase ?? null,
7495
7587
  token: ctx.token,
7496
7588
  user: ctx.user,
7589
+ devRouterPort: taskContext.devPort,
7497
7590
  });
7498
7591
  }
7499
7592
  catch (error) {
@@ -7584,8 +7677,8 @@ async function completeTask(options) {
7584
7677
  throw new Error("task_done_summary_too_long:max_200_characters");
7585
7678
  }
7586
7679
  if (taskContext.gameSourceGit) {
7587
- if (!(0, node_fs_1.existsSync)(workerGameSourceReadinessPath(workspaceDir)))
7588
- throw new Error("task_done_source_not_ready");
7680
+ const uploadResult = readTaskUploadResultFile(workspaceDir);
7681
+ assertTaskUploadResultMatchesContext({ taskContext, uploadResult });
7589
7682
  const ctx = await resolveTaskCommandContext("task done", options.env, taskContext);
7590
7683
  if (!ctx)
7591
7684
  return;
@@ -7606,8 +7699,20 @@ async function completeTask(options) {
7606
7699
  ...(nextSteps !== undefined ? { nextSteps } : {}),
7607
7700
  ...(platformFeedback ? { platformFeedback } : {}),
7608
7701
  };
7609
- await (0, promises_1.writeFile)(workerGameSourceCompletionPath(workspaceDir), `${JSON.stringify(intent, null, 2)}\n`, "utf8");
7610
- (0, output_1.printSuccess)("Task completion recorded. The worker will reconcile and publish the source after the agent exits.");
7702
+ await withTaskUploadLock(workspaceDir, async () => {
7703
+ const result = await (0, game_source_commands_1.requestWorkerGameSourceCommand)({
7704
+ workspaceDir,
7705
+ taskId: taskContext.taskId,
7706
+ attempt: taskContext.attempt,
7707
+ command: "done",
7708
+ payload: intent,
7709
+ });
7710
+ if (!result || typeof result !== "object" || !("completed" in result) || result.completed !== true) {
7711
+ throw new Error("game_source_completion_response_invalid");
7712
+ }
7713
+ await (0, promises_1.writeFile)(workerGameSourceCompletionPath(workspaceDir), `${JSON.stringify(intent, null, 2)}\n`, "utf8");
7714
+ });
7715
+ (0, output_1.printSuccess)("Task marked done.");
7611
7716
  return;
7612
7717
  }
7613
7718
  const uploadResult = readTaskUploadResultFile();
@@ -7955,7 +8060,7 @@ async function failTask(options) {
7955
8060
  const classifiedMessage = (0, types_1.classifyAgentTaskTerminalReason)(message, "agent");
7956
8061
  const terminalReason = uploadFailure?.terminalReason ?? {
7957
8062
  ...classifiedMessage,
7958
- message: "The agent reported that it could not complete the task.",
8063
+ message,
7959
8064
  retryable: options.retryableWithSameInput === true,
7960
8065
  source: "agent",
7961
8066
  };