@playdrop/playdrop-cli 0.12.7 → 0.12.9

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.
@@ -16,9 +16,11 @@ exports.appendWorkerTranscriptChunks = appendWorkerTranscriptChunks;
16
16
  exports.readTaskNextStepsFile = readTaskNextStepsFile;
17
17
  exports.assertTaskUploadResultMatchesContext = assertTaskUploadResultMatchesContext;
18
18
  exports.stageAssignmentWorkspaceFiles = stageAssignmentWorkspaceFiles;
19
- exports.resolvePlaydropPluginRoot = resolvePlaydropPluginRoot;
20
19
  exports.stagePlaydropPluginReferences = stagePlaydropPluginReferences;
20
+ exports.stageCodexTaskSkills = stageCodexTaskSkills;
21
+ exports.assertAssignmentPluginBundleExtractionBounds = assertAssignmentPluginBundleExtractionBounds;
21
22
  exports.stageAssignmentPluginBundle = stageAssignmentPluginBundle;
23
+ exports.resolveDevPluginWorkingTree = resolveDevPluginWorkingTree;
22
24
  exports.discoverWorkerProjectRoot = discoverWorkerProjectRoot;
23
25
  exports.readWorkerTaskState = readWorkerTaskState;
24
26
  exports.buildWorkerHealthAlertText = buildWorkerHealthAlertText;
@@ -28,6 +30,7 @@ exports.isWorkerTransientNetworkError = isWorkerTransientNetworkError;
28
30
  exports.classifyWorkerHeartbeatError = classifyWorkerHeartbeatError;
29
31
  exports.assertWorkerClaimErrorRetryable = assertWorkerClaimErrorRetryable;
30
32
  exports.resolvePlaydropAssetRequirementFromText = resolvePlaydropAssetRequirementFromText;
33
+ exports.resolvePlaywrightBrowsersPathFromExecutable = resolvePlaywrightBrowsersPathFromExecutable;
31
34
  exports.loadWorkerEnvFile = loadWorkerEnvFile;
32
35
  exports.buildWorkerCapabilities = buildWorkerCapabilities;
33
36
  exports.buildWorkerClaimBody = buildWorkerClaimBody;
@@ -36,12 +39,23 @@ exports.extractZipArchive = extractZipArchive;
36
39
  exports.readWorkerProjectVersion = readWorkerProjectVersion;
37
40
  exports.buildCataloguePreviewPayload = buildCataloguePreviewPayload;
38
41
  exports.assertWorkerProjectVersionBumped = assertWorkerProjectVersionBumped;
42
+ exports.prepareCodexRunHomes = prepareCodexRunHomes;
43
+ exports.prepareClaudeRunConfig = prepareClaudeRunConfig;
44
+ exports.removeAgentRunCredentials = removeAgentRunCredentials;
45
+ exports.removeStaleAgentRunCredentials = removeStaleAgentRunCredentials;
39
46
  exports.buildAgentFailureCode = buildAgentFailureCode;
40
47
  exports.describeAgentFailureForEvent = describeAgentFailureForEvent;
41
48
  exports.createLocalPlaydropShim = createLocalPlaydropShim;
42
49
  exports.parseCodexModelCatalog = parseCodexModelCatalog;
43
50
  exports.parseClaudeModelCatalog = parseClaudeModelCatalog;
51
+ exports.claudeModelProbeReportsSelection = claudeModelProbeReportsSelection;
44
52
  exports.buildWorkerSetupActions = buildWorkerSetupActions;
53
+ exports.resolveWorkerStateDir = resolveWorkerStateDir;
54
+ exports.resolveWorkerSupervisorLockFile = resolveWorkerSupervisorLockFile;
55
+ exports.parseLinuxProcessStartIdentity = parseLinuxProcessStartIdentity;
56
+ exports.removeStaleWorkerSupervisorLockIfUnchanged = removeStaleWorkerSupervisorLockIfUnchanged;
57
+ exports.acquireWorkerSupervisorLock = acquireWorkerSupervisorLock;
58
+ exports.releaseWorkerSupervisorLock = releaseWorkerSupervisorLock;
45
59
  exports.readActivePersonalWorkerRuntimeState = readActivePersonalWorkerRuntimeState;
46
60
  exports.parseLaunchAgentProgramArguments = parseLaunchAgentProgramArguments;
47
61
  exports.assertLaunchAgentPlistCompatible = assertLaunchAgentPlistCompatible;
@@ -78,7 +92,6 @@ const config_1 = require("../config");
78
92
  const messages_1 = require("../messages");
79
93
  const output_1 = require("../output");
80
94
  const shellProbe_1 = require("../shellProbe");
81
- const agents_1 = require("./agents");
82
95
  const review_1 = require("./review");
83
96
  const upload_1 = require("./upload");
84
97
  const runtime_1 = require("./worker/runtime");
@@ -117,9 +130,14 @@ const WORKER_SUPPORTED_KINDS = ['NEW_GAME', 'REMIX_GAME', 'GAME_UPDATE', 'GAME_R
117
130
  const PLAYDROP_PLUGIN_MANIFEST_REQUIRED_STARTUP_KINDS = WORKER_SUPPORTED_KINDS;
118
131
  const requireFromWorker = (0, node_module_1.createRequire)(__filename);
119
132
  const STAGED_PLAYDROP_PLUGIN_ROOT = '.playdrop/plugin';
133
+ const STAGED_CODEX_SKILL_ROOT = '.agents/skills';
120
134
  const PLAYDROP_PLUGIN_STAGING_MANIFEST_PATH = 'playdrop-worker-staging.json';
121
135
  const ASSIGNMENT_PLUGIN_BUNDLE_MAX_FILES = 1000;
122
136
  const ASSIGNMENT_PLUGIN_BUNDLE_MAX_UNCOMPRESSED_BYTES = 20 * 1024 * 1024;
137
+ const ZIP_CENTRAL_DIRECTORY_SIGNATURE = 0x02014b50;
138
+ const ZIP_END_OF_CENTRAL_DIRECTORY_SIGNATURE = 0x06054b50;
139
+ const ZIP_END_OF_CENTRAL_DIRECTORY_MIN_BYTES = 22;
140
+ const ZIP_MAX_COMMENT_BYTES = 0xffff;
123
141
  const execFileAsync = (0, node_util_1.promisify)(node_child_process_1.execFile);
124
142
  exports.WORKER_SESSION_EXPIRED_MESSAGE = 'worker session expired: run "playdrop auth login" and start the worker again';
125
143
  const WORKER_CONTEXT_ALLOWED_COMMANDS = [
@@ -335,6 +353,9 @@ function resolveWorkerClaimTaskAssignment(claim) {
335
353
  const pluginBundle = assignment.pluginBundle && typeof assignment.pluginBundle === 'object' && !Array.isArray(assignment.pluginBundle)
336
354
  ? assignment.pluginBundle
337
355
  : null;
356
+ if (!pluginBundle) {
357
+ throw new Error('agent_task_assignment_plugin_bundle_missing');
358
+ }
338
359
  return {
339
360
  taskId,
340
361
  taskToken,
@@ -383,7 +404,7 @@ function resolveWorkerHomeDir() {
383
404
  return node_path_1.default.join(node_os_1.default.homedir(), 'PlayDropWorker');
384
405
  }
385
406
  function workerStateFilePath() {
386
- return node_path_1.default.join(resolveWorkerHomeDir(), 'state', 'current-task.json');
407
+ return node_path_1.default.join(resolveWorkerStateDir(), 'current-task.json');
387
408
  }
388
409
  function workerTaskWorkspaceDirFromAssignment(assignment) {
389
410
  return node_path_1.default.join(resolveWorkerHomeDir(), 'tasks', assignment.workspace.folderName);
@@ -828,23 +849,6 @@ async function stageAssignmentWorkspaceFiles(workspaceDir, files) {
828
849
  await (0, promises_1.writeFile)(destination, Buffer.from(file.contentBase64, 'base64'));
829
850
  }
830
851
  }
831
- function candidateVersionDirectories(root) {
832
- if (!(0, node_fs_1.existsSync)(root)) {
833
- return [];
834
- }
835
- return (0, node_fs_1.readdirSync)(root)
836
- .filter((name) => name.trim().length > 0)
837
- .sort((left, right) => right.localeCompare(left, undefined, { numeric: true }))
838
- .map((name) => node_path_1.default.join(root, name));
839
- }
840
- function isDirectory(candidatePath) {
841
- try {
842
- return (0, node_fs_1.statSync)(candidatePath).isDirectory();
843
- }
844
- catch {
845
- return false;
846
- }
847
- }
848
852
  function isFile(candidatePath) {
849
853
  try {
850
854
  return (0, node_fs_1.statSync)(candidatePath).isFile();
@@ -853,13 +857,6 @@ function isFile(candidatePath) {
853
857
  return false;
854
858
  }
855
859
  }
856
- function looksLikePlaydropPluginRoot(pluginRoot) {
857
- return isDirectory(pluginRoot) && ((0, node_fs_1.existsSync)(node_path_1.default.join(pluginRoot, PLAYDROP_PLUGIN_STAGING_MANIFEST_PATH))
858
- || isDirectory(node_path_1.default.join(pluginRoot, 'skills'))
859
- || isFile(node_path_1.default.join(pluginRoot, '.codex-plugin', 'plugin.json'))
860
- || isFile(node_path_1.default.join(pluginRoot, '.claude-plugin', 'plugin.json'))
861
- || isFile(node_path_1.default.join(pluginRoot, '.cursor-plugin', 'plugin.json')));
862
- }
863
860
  function normalizePlaydropPluginManifestPath(input) {
864
861
  if (typeof input.value !== 'string' || input.value.trim().length <= 0) {
865
862
  throw new Error(`playdrop_plugin_staging_manifest_incomplete:${input.kind}: file paths must be non-empty strings in ${input.manifestPath}`);
@@ -958,45 +955,6 @@ function readPlaydropPluginStagingManifest(pluginRoot) {
958
955
  taskFiles: taskFiles,
959
956
  };
960
957
  }
961
- function resolvePlaydropPluginRoot(input = {}) {
962
- const homeDir = input.homeDir ?? node_os_1.default.homedir();
963
- const env = input.env ?? node_process_1.default.env;
964
- const explicitRoot = env.PLAYDROP_WORKER_PLAYDROP_PLUGIN_DIR?.trim();
965
- if (explicitRoot) {
966
- const resolved = node_path_1.default.resolve(explicitRoot);
967
- readPlaydropPluginStagingManifest(resolved);
968
- return resolved;
969
- }
970
- const candidates = [
971
- node_path_1.default.join(homeDir, 'Documents', 'playdrop-plugin'),
972
- node_path_1.default.join(homeDir, '.claude', 'plugins', 'marketplaces', 'playdrop'),
973
- ...candidateVersionDirectories(node_path_1.default.join(homeDir, '.claude', 'plugins', 'cache', 'playdrop', 'playdrop')),
974
- node_path_1.default.join(homeDir, '.codex', 'plugins', 'playdrop'),
975
- node_path_1.default.join(homeDir, '.codex', 'plugins', 'cache', 'playdrop', 'playdrop'),
976
- ...candidateVersionDirectories(node_path_1.default.join(homeDir, '.codex', 'plugins', 'cache', 'playdrop', 'playdrop')),
977
- ...candidateVersionDirectories(node_path_1.default.join(homeDir, '.codex', 'plugins', 'cache', 'olivier-local-plugins', 'playdrop')),
978
- ];
979
- let firstCandidateError = null;
980
- for (const candidate of candidates) {
981
- if (!looksLikePlaydropPluginRoot(candidate)) {
982
- continue;
983
- }
984
- try {
985
- readPlaydropPluginStagingManifest(candidate);
986
- return candidate;
987
- }
988
- catch (error) {
989
- firstCandidateError ?? (firstCandidateError = error);
990
- continue;
991
- }
992
- }
993
- if (firstCandidateError) {
994
- throw firstCandidateError instanceof Error
995
- ? firstCandidateError
996
- : new Error(String(firstCandidateError));
997
- }
998
- throw new Error(`playdrop_plugin_staging_manifest_missing: install or update the PlayDrop plugin so ${PLAYDROP_PLUGIN_STAGING_MANIFEST_PATH} is available.`);
999
- }
1000
958
  async function stagePlaydropPluginReferences(input) {
1001
959
  const pluginRoot = node_path_1.default.resolve(input.pluginRoot);
1002
960
  if (!WORKER_SUPPORTED_KINDS.includes(input.kind)) {
@@ -1017,6 +975,93 @@ async function stagePlaydropPluginReferences(input) {
1017
975
  }
1018
976
  return staged;
1019
977
  }
978
+ async function stageCodexTaskSkills(input) {
979
+ const skillRoot = node_path_1.default.join(input.workspaceDir, STAGED_CODEX_SKILL_ROOT);
980
+ await (0, promises_1.rm)(skillRoot, { recursive: true, force: true });
981
+ const stagedSkills = [];
982
+ for (const stagedPluginPath of input.stagedPluginPaths) {
983
+ const normalized = stagedPluginPath.replace(/\\/g, '/');
984
+ const match = /^\.playdrop\/plugin\/skills\/([^/]+)\/SKILL\.md$/.exec(normalized);
985
+ if (!match?.[1]) {
986
+ continue;
987
+ }
988
+ const source = resolveWorkspaceFileDestination(input.workspaceDir, normalized);
989
+ const destinationRelativePath = `${STAGED_CODEX_SKILL_ROOT}/${match[1]}/SKILL.md`;
990
+ const destination = resolveWorkspaceFileDestination(input.workspaceDir, destinationRelativePath);
991
+ await (0, promises_1.mkdir)(node_path_1.default.dirname(destination), { recursive: true });
992
+ await (0, promises_1.copyFile)(source, destination);
993
+ stagedSkills.push(destinationRelativePath);
994
+ }
995
+ if (stagedSkills.length <= 0) {
996
+ throw new Error('agent_task_codex_skills_missing');
997
+ }
998
+ return stagedSkills.sort();
999
+ }
1000
+ function assertAssignmentPluginBundleExtractionBounds(buffer) {
1001
+ if (buffer.length < ZIP_END_OF_CENTRAL_DIRECTORY_MIN_BYTES) {
1002
+ throw new Error('agent_task_plugin_bundle_invalid_zip');
1003
+ }
1004
+ const earliestEndRecordOffset = Math.max(0, buffer.length - ZIP_END_OF_CENTRAL_DIRECTORY_MIN_BYTES - ZIP_MAX_COMMENT_BYTES);
1005
+ let endRecordOffset = -1;
1006
+ for (let offset = buffer.length - ZIP_END_OF_CENTRAL_DIRECTORY_MIN_BYTES; offset >= earliestEndRecordOffset; offset -= 1) {
1007
+ if (buffer.readUInt32LE(offset) === ZIP_END_OF_CENTRAL_DIRECTORY_SIGNATURE
1008
+ && offset + ZIP_END_OF_CENTRAL_DIRECTORY_MIN_BYTES + buffer.readUInt16LE(offset + 20) === buffer.length) {
1009
+ endRecordOffset = offset;
1010
+ break;
1011
+ }
1012
+ }
1013
+ if (endRecordOffset < 0) {
1014
+ throw new Error('agent_task_plugin_bundle_invalid_zip');
1015
+ }
1016
+ const diskNumber = buffer.readUInt16LE(endRecordOffset + 4);
1017
+ const centralDirectoryDisk = buffer.readUInt16LE(endRecordOffset + 6);
1018
+ const diskEntryCount = buffer.readUInt16LE(endRecordOffset + 8);
1019
+ const entryCount = buffer.readUInt16LE(endRecordOffset + 10);
1020
+ const centralDirectorySize = buffer.readUInt32LE(endRecordOffset + 12);
1021
+ const centralDirectoryOffset = buffer.readUInt32LE(endRecordOffset + 16);
1022
+ if (diskNumber !== 0
1023
+ || centralDirectoryDisk !== 0
1024
+ || diskEntryCount !== entryCount
1025
+ || entryCount === 0xffff
1026
+ || centralDirectorySize === 0xffffffff
1027
+ || centralDirectoryOffset === 0xffffffff) {
1028
+ throw new Error('agent_task_plugin_bundle_unsupported_zip');
1029
+ }
1030
+ if (entryCount > ASSIGNMENT_PLUGIN_BUNDLE_MAX_FILES) {
1031
+ throw new Error(`agent_task_plugin_bundle_too_many_files:${entryCount}>${ASSIGNMENT_PLUGIN_BUNDLE_MAX_FILES}`);
1032
+ }
1033
+ if (centralDirectoryOffset + centralDirectorySize !== endRecordOffset) {
1034
+ throw new Error('agent_task_plugin_bundle_invalid_zip');
1035
+ }
1036
+ let cursor = centralDirectoryOffset;
1037
+ let uncompressedBytes = 0;
1038
+ for (let entryIndex = 0; entryIndex < entryCount; entryIndex += 1) {
1039
+ if (cursor + 46 > endRecordOffset
1040
+ || buffer.readUInt32LE(cursor) !== ZIP_CENTRAL_DIRECTORY_SIGNATURE) {
1041
+ throw new Error('agent_task_plugin_bundle_invalid_zip');
1042
+ }
1043
+ const compressedSize = buffer.readUInt32LE(cursor + 20);
1044
+ const uncompressedSize = buffer.readUInt32LE(cursor + 24);
1045
+ const fileNameLength = buffer.readUInt16LE(cursor + 28);
1046
+ const extraFieldLength = buffer.readUInt16LE(cursor + 30);
1047
+ const fileCommentLength = buffer.readUInt16LE(cursor + 32);
1048
+ if (compressedSize === 0xffffffff || uncompressedSize === 0xffffffff) {
1049
+ throw new Error('agent_task_plugin_bundle_unsupported_zip');
1050
+ }
1051
+ const nextCursor = cursor + 46 + fileNameLength + extraFieldLength + fileCommentLength;
1052
+ if (nextCursor > endRecordOffset) {
1053
+ throw new Error('agent_task_plugin_bundle_invalid_zip');
1054
+ }
1055
+ uncompressedBytes += uncompressedSize;
1056
+ if (uncompressedBytes > ASSIGNMENT_PLUGIN_BUNDLE_MAX_UNCOMPRESSED_BYTES) {
1057
+ throw new Error(`agent_task_plugin_bundle_too_large:${uncompressedBytes}>${ASSIGNMENT_PLUGIN_BUNDLE_MAX_UNCOMPRESSED_BYTES}`);
1058
+ }
1059
+ cursor = nextCursor;
1060
+ }
1061
+ if (cursor !== endRecordOffset) {
1062
+ throw new Error('agent_task_plugin_bundle_invalid_zip');
1063
+ }
1064
+ }
1020
1065
  async function stageAssignmentPluginBundle(input) {
1021
1066
  const sha256 = typeof input.pluginBundle.sha256 === 'string' ? input.pluginBundle.sha256.trim() : '';
1022
1067
  const contentBase64 = typeof input.pluginBundle.contentBase64 === 'string' ? input.pluginBundle.contentBase64.trim() : '';
@@ -1031,6 +1076,7 @@ async function stageAssignmentPluginBundle(input) {
1031
1076
  if (actualSha !== sha256) {
1032
1077
  throw new Error('agent_task_plugin_bundle_sha_mismatch');
1033
1078
  }
1079
+ assertAssignmentPluginBundleExtractionBounds(buffer);
1034
1080
  const pluginRoot = node_path_1.default.join(input.workspaceDir, '.playdrop', 'task-plugin');
1035
1081
  await (0, promises_1.rm)(pluginRoot, { recursive: true, force: true });
1036
1082
  await (0, promises_1.mkdir)(pluginRoot, { recursive: true });
@@ -1065,6 +1111,23 @@ async function stageAssignmentPluginBundle(input) {
1065
1111
  readPlaydropPluginStagingManifest(pluginRoot);
1066
1112
  return pluginRoot;
1067
1113
  }
1114
+ function resolveDevPluginWorkingTree(input) {
1115
+ const workingTree = input.workingTree?.trim();
1116
+ if (!workingTree) {
1117
+ return null;
1118
+ }
1119
+ const nodeEnv = input.nodeEnv?.trim().toLowerCase() ?? '';
1120
+ const env = input.env.trim().toLowerCase();
1121
+ if (nodeEnv === 'production' || env === 'prod' || env === 'production') {
1122
+ throw new Error(`worker_dev_plugin_working_tree_refused_in_production:${input.env}:${input.nodeEnv ?? ''}`);
1123
+ }
1124
+ if (env !== 'dev') {
1125
+ throw new Error(`worker_dev_plugin_working_tree_requires_dev_environment:${input.env}`);
1126
+ }
1127
+ const resolved = node_path_1.default.resolve(workingTree);
1128
+ readPlaydropPluginStagingManifest(resolved);
1129
+ return resolved;
1130
+ }
1068
1131
  // A PlayDrop project root is the topmost directory holding a catalogue.json,
1069
1132
  // the same marker "playdrop project publish" resolves its target from. Nested
1070
1133
  // catalogue.json files belong to the root above them, so the walk stops at the
@@ -1335,6 +1398,29 @@ function probePlaywrightChromium() {
1335
1398
  }
1336
1399
  return { version, chromiumInstalled: true, executablePath };
1337
1400
  }
1401
+ function resolvePlaywrightBrowsersPathFromExecutable(executablePath) {
1402
+ let current = node_path_1.default.resolve(executablePath);
1403
+ while (true) {
1404
+ const parent = node_path_1.default.dirname(current);
1405
+ if (parent === current) {
1406
+ throw new Error(`worker_playwright_cache_root_unresolved:${executablePath}`);
1407
+ }
1408
+ if (/^chromium-\d+$/.test(node_path_1.default.basename(parent))) {
1409
+ return node_path_1.default.dirname(parent);
1410
+ }
1411
+ current = parent;
1412
+ }
1413
+ }
1414
+ function resolveWorkerPlaywrightBrowsersPath() {
1415
+ let playwrightCore;
1416
+ try {
1417
+ playwrightCore = requireFromWorker('playwright-core');
1418
+ }
1419
+ catch {
1420
+ throw new Error('worker_preflight_playwright_missing: install Playwright before starting the worker.');
1421
+ }
1422
+ return resolvePlaywrightBrowsersPathFromExecutable(playwrightCore.chromium.executablePath());
1423
+ }
1338
1424
  function normalizeDiscoveredModels(agent, models) {
1339
1425
  return Array.from(new Set(models.map((model) => (0, types_1.normalizeAgentTaskModel)(agent, model)).filter(Boolean)));
1340
1426
  }
@@ -1785,7 +1871,159 @@ function resolveClaudeRuntimeModel(model) {
1785
1871
  }
1786
1872
  return model;
1787
1873
  }
1874
+ async function prepareCodexRunHomes(input) {
1875
+ const runRoot = node_path_1.default.join(input.workspaceDir, '.playdrop', 'codex-run');
1876
+ const homeDir = node_path_1.default.join(runRoot, 'home');
1877
+ const codexHomeDir = node_path_1.default.join(runRoot, 'codex-home');
1878
+ await (0, promises_1.rm)(runRoot, { recursive: true, force: true });
1879
+ await (0, promises_1.mkdir)(runRoot, { recursive: true, mode: 0o700 });
1880
+ await (0, promises_1.chmod)(runRoot, 0o700);
1881
+ await (0, promises_1.mkdir)(homeDir, { mode: 0o700 });
1882
+ await (0, promises_1.mkdir)(codexHomeDir, { mode: 0o700 });
1883
+ const sourceCodexHome = node_path_1.default.resolve(input.sourceCodexHome?.trim()
1884
+ || node_process_1.default.env.CODEX_HOME?.trim()
1885
+ || node_path_1.default.join(node_os_1.default.homedir(), '.codex'));
1886
+ const sourceAuthPath = node_path_1.default.join(sourceCodexHome, 'auth.json');
1887
+ if (!isFile(sourceAuthPath)) {
1888
+ throw new Error(`worker_codex_auth_missing:${sourceAuthPath}`);
1889
+ }
1890
+ const stagedAuthPath = node_path_1.default.join(codexHomeDir, 'auth.json');
1891
+ await (0, promises_1.copyFile)(sourceAuthPath, stagedAuthPath);
1892
+ await (0, promises_1.chmod)(stagedAuthPath, 0o600);
1893
+ await stageWorkerPlaydropSession({
1894
+ homeDir,
1895
+ workerUsername: input.workerUsername,
1896
+ envName: input.envName,
1897
+ playdropConfig: input.playdropConfig,
1898
+ });
1899
+ return { homeDir, codexHomeDir };
1900
+ }
1901
+ async function stageWorkerPlaydropSession(input) {
1902
+ const workerUsername = input.workerUsername.trim();
1903
+ const envName = input.envName.trim();
1904
+ const config = input.playdropConfig ?? (0, config_1.loadConfig)();
1905
+ const session = config.accounts?.[workerUsername]?.[envName];
1906
+ if (!workerUsername || !envName || !session?.token?.trim()) {
1907
+ throw new Error(`worker_playdrop_session_missing:${workerUsername || 'unknown'}:${envName || 'unknown'}`);
1908
+ }
1909
+ const playdropConfigDir = node_path_1.default.join(input.homeDir, '.playdrop');
1910
+ const playdropConfigPath = node_path_1.default.join(playdropConfigDir, 'config.json');
1911
+ await (0, promises_1.mkdir)(playdropConfigDir, { recursive: true, mode: 0o700 });
1912
+ await (0, promises_1.chmod)(playdropConfigDir, 0o700);
1913
+ await (0, promises_1.writeFile)(playdropConfigPath, JSON.stringify({
1914
+ version: 2,
1915
+ currentAccount: { username: workerUsername, env: envName },
1916
+ accounts: {
1917
+ [workerUsername]: {
1918
+ [envName]: {
1919
+ token: session.token.trim(),
1920
+ updatedAt: session.updatedAt,
1921
+ },
1922
+ },
1923
+ },
1924
+ }, null, 2), { mode: 0o600 });
1925
+ await (0, promises_1.chmod)(playdropConfigPath, 0o600);
1926
+ }
1927
+ async function prepareClaudeRunConfig(input) {
1928
+ const runRoot = node_path_1.default.join(input.workspaceDir, '.playdrop', 'claude-run');
1929
+ const homeDir = node_path_1.default.join(runRoot, 'home');
1930
+ const claudeConfigDir = node_path_1.default.join(runRoot, 'config');
1931
+ await (0, promises_1.rm)(runRoot, { recursive: true, force: true });
1932
+ await (0, promises_1.mkdir)(runRoot, { recursive: true, mode: 0o700 });
1933
+ await (0, promises_1.chmod)(runRoot, 0o700);
1934
+ await (0, promises_1.mkdir)(homeDir, { mode: 0o700 });
1935
+ await (0, promises_1.mkdir)(claudeConfigDir, { mode: 0o700 });
1936
+ const configuredCredentialsPath = input.sourceCredentialsPath?.trim()
1937
+ || node_process_1.default.env.PLAYDROP_WORKER_CLAUDE_CREDENTIALS_FILE?.trim();
1938
+ const defaultCredentialsPath = node_path_1.default.join(node_os_1.default.homedir(), '.claude', '.credentials.json');
1939
+ let rawCredentials;
1940
+ if (configuredCredentialsPath) {
1941
+ const sourceCredentialsPath = node_path_1.default.resolve(configuredCredentialsPath);
1942
+ if (!isFile(sourceCredentialsPath)) {
1943
+ throw new Error(`worker_claude_credentials_missing:${sourceCredentialsPath}`);
1944
+ }
1945
+ rawCredentials = (0, node_fs_1.readFileSync)(sourceCredentialsPath, 'utf8');
1946
+ }
1947
+ else if (isFile(defaultCredentialsPath)) {
1948
+ rawCredentials = (0, node_fs_1.readFileSync)(defaultCredentialsPath, 'utf8');
1949
+ }
1950
+ else if (node_process_1.default.platform === 'darwin') {
1951
+ try {
1952
+ const result = await execFileAsync('security', [
1953
+ 'find-generic-password',
1954
+ '-s',
1955
+ 'Claude Code-credentials',
1956
+ '-w',
1957
+ ], { maxBuffer: 1024 * 1024 });
1958
+ rawCredentials = result.stdout;
1959
+ }
1960
+ catch {
1961
+ throw new Error('worker_claude_credentials_missing:macos_keychain');
1962
+ }
1963
+ }
1964
+ else {
1965
+ throw new Error(`worker_claude_credentials_missing:${defaultCredentialsPath}`);
1966
+ }
1967
+ let credentials;
1968
+ try {
1969
+ credentials = JSON.parse(rawCredentials);
1970
+ }
1971
+ catch {
1972
+ throw new Error('worker_claude_credentials_invalid_json');
1973
+ }
1974
+ const credentialObject = typeof credentials === 'object' && credentials !== null && !Array.isArray(credentials)
1975
+ ? credentials
1976
+ : null;
1977
+ const oauthValue = credentialObject?.claudeAiOauth;
1978
+ const claudeAiOauth = typeof oauthValue === 'object' && oauthValue !== null && !Array.isArray(oauthValue)
1979
+ ? oauthValue
1980
+ : null;
1981
+ if (!claudeAiOauth
1982
+ || typeof claudeAiOauth.accessToken !== 'string'
1983
+ || !claudeAiOauth.accessToken.trim()
1984
+ || typeof claudeAiOauth.refreshToken !== 'string'
1985
+ || !claudeAiOauth.refreshToken.trim()) {
1986
+ throw new Error('worker_claude_oauth_credentials_missing');
1987
+ }
1988
+ const stagedCredentialsPath = node_path_1.default.join(claudeConfigDir, '.credentials.json');
1989
+ await (0, promises_1.writeFile)(stagedCredentialsPath, `${JSON.stringify({ claudeAiOauth })}\n`, { mode: 0o600 });
1990
+ await (0, promises_1.chmod)(stagedCredentialsPath, 0o600);
1991
+ await stageWorkerPlaydropSession({
1992
+ homeDir,
1993
+ workerUsername: input.workerUsername,
1994
+ envName: input.envName,
1995
+ playdropConfig: input.playdropConfig,
1996
+ });
1997
+ return { homeDir, claudeConfigDir };
1998
+ }
1999
+ async function removeAgentRunCredentials(workspaceDir) {
2000
+ await Promise.all([
2001
+ (0, promises_1.rm)(node_path_1.default.join(workspaceDir, '.playdrop', 'codex-run'), { recursive: true, force: true }),
2002
+ (0, promises_1.rm)(node_path_1.default.join(workspaceDir, '.playdrop', 'claude-run'), { recursive: true, force: true }),
2003
+ ]);
2004
+ }
2005
+ async function removeStaleAgentRunCredentials(workerHomeDir = resolveWorkerHomeDir()) {
2006
+ const tasksDir = node_path_1.default.join(workerHomeDir, 'tasks');
2007
+ let entries;
2008
+ try {
2009
+ entries = await (0, promises_1.readdir)(tasksDir, { withFileTypes: true });
2010
+ }
2011
+ catch (error) {
2012
+ if (error.code === 'ENOENT') {
2013
+ return;
2014
+ }
2015
+ throw error;
2016
+ }
2017
+ await Promise.all(entries
2018
+ .filter((entry) => entry.isDirectory())
2019
+ .map((entry) => removeAgentRunCredentials(node_path_1.default.join(tasksDir, entry.name))));
2020
+ }
1788
2021
  async function runCodex(input) {
2022
+ const { homeDir, codexHomeDir } = await prepareCodexRunHomes({
2023
+ workspaceDir: input.workspaceDir,
2024
+ workerUsername: input.workerUsername,
2025
+ envName: input.envName,
2026
+ });
1789
2027
  let observedCodexThreadId = null;
1790
2028
  let codexThreadProbe = '';
1791
2029
  const result = await (0, runtime_1.runLoggedProcess)({
@@ -1805,6 +2043,9 @@ async function runCodex(input) {
1805
2043
  envName: input.envName,
1806
2044
  eventDir: input.eventDir,
1807
2045
  devPort: input.devPort,
2046
+ homeDir,
2047
+ codexHomeDir,
2048
+ playwrightBrowsersPath: resolveWorkerPlaywrightBrowsersPath(),
1808
2049
  }),
1809
2050
  stdin: input.prompt,
1810
2051
  timeoutMs: (0, runtime_1.readPositiveEnvInt)('PLAYDROP_WORKER_CODEX_TIMEOUT_MS', runtime_1.DEFAULT_CODEX_TIMEOUT_MS),
@@ -1827,7 +2068,9 @@ async function runCodex(input) {
1827
2068
  }
1828
2069
  return result;
1829
2070
  }
1830
- const rolloutUsage = await (0, runtime_1.readCodexRolloutTokenUsageForThread)(observedCodexThreadId);
2071
+ const rolloutUsage = await (0, runtime_1.readCodexRolloutTokenUsageForThread)(observedCodexThreadId, {
2072
+ sessionsRoot: node_path_1.default.join(codexHomeDir, 'sessions'),
2073
+ });
1831
2074
  if (rolloutUsage.usageParseError || rolloutUsage.totalTokens === null) {
1832
2075
  if (result.tokenUsage.usageParseError) {
1833
2076
  console.warn(`Codex rollout token usage could not be read for thread ${observedCodexThreadId}: ${rolloutUsage.usageParseError ?? 'unknown_error'}.`);
@@ -1846,6 +2089,11 @@ async function runCodex(input) {
1846
2089
  };
1847
2090
  }
1848
2091
  async function runClaude(input) {
2092
+ const { homeDir, claudeConfigDir } = await prepareClaudeRunConfig({
2093
+ workspaceDir: input.workspaceDir,
2094
+ workerUsername: input.workerUsername,
2095
+ envName: input.envName,
2096
+ });
1849
2097
  const denyReadRoots = resolveClaudeDenyReadRoots(input.workspaceDir);
1850
2098
  const screenshotCollector = input.enableChrome
1851
2099
  ? (0, instrument_evidence_1.createWorkerInstrumentScreenshotCollector)(input.workspaceDir)
@@ -1856,7 +2104,7 @@ async function runClaude(input) {
1856
2104
  ...input.claudeModel,
1857
2105
  workspaceDir: input.workspaceDir,
1858
2106
  denyReadRoots,
1859
- pluginDir: input.playdropPluginRoot,
2107
+ pluginDir: input.taskPluginRoot,
1860
2108
  chrome: input.enableChrome,
1861
2109
  }),
1862
2110
  cwd: input.workspaceDir,
@@ -1867,6 +2115,9 @@ async function runClaude(input) {
1867
2115
  envName: input.envName,
1868
2116
  eventDir: input.eventDir,
1869
2117
  devPort: input.devPort,
2118
+ homeDir,
2119
+ claudeConfigDir,
2120
+ playwrightBrowsersPath: resolveWorkerPlaywrightBrowsersPath(),
1870
2121
  }),
1871
2122
  stdin: input.prompt,
1872
2123
  timeoutMs: (0, runtime_1.readPositiveEnvInt)('PLAYDROP_WORKER_CLAUDE_TIMEOUT_MS', runtime_1.DEFAULT_CODEX_TIMEOUT_MS),
@@ -2204,12 +2455,30 @@ function parseClaudeModelCatalog(output) {
2204
2455
  }
2205
2456
  return normalized;
2206
2457
  }
2458
+ function claudeModelProbeReportsSelection(output, expectedDisplayName) {
2459
+ let parsed;
2460
+ try {
2461
+ parsed = JSON.parse(output);
2462
+ }
2463
+ catch {
2464
+ return false;
2465
+ }
2466
+ const result = typeof parsed === 'object' && parsed !== null && typeof parsed.result === 'string'
2467
+ ? parsed.result
2468
+ : '';
2469
+ return result.split(/\r?\n/, 1)[0]?.trim() === `Current model: ${expectedDisplayName}`;
2470
+ }
2207
2471
  function probeClaudeModels() {
2208
2472
  const result = (0, shellProbe_1.runShell)('claude --print --output-format json --max-turns 1 "/model"');
2209
2473
  if (result.exitCode !== 0) {
2210
2474
  throw new Error(`worker_preflight_claude_model_catalog_failed: "claude /model" exited with code ${result.exitCode}. Output: ${result.output.slice(0, 200)}`);
2211
2475
  }
2212
- return parseClaudeModelCatalog(result.output);
2476
+ const discovered = parseClaudeModelCatalog(result.output);
2477
+ const sonnet5 = (0, shellProbe_1.runShell)('claude --model claude-sonnet-5 --print --output-format json --max-turns 1 "/model"');
2478
+ if (sonnet5.exitCode === 0 && claudeModelProbeReportsSelection(sonnet5.output, 'Sonnet 5')) {
2479
+ discovered.push('claude-sonnet-5');
2480
+ }
2481
+ return normalizeDiscoveredModels('CLAUDE_CODE', discovered);
2213
2482
  }
2214
2483
  function probeReadyWorkerCapabilities(input) {
2215
2484
  const codexVersion = probeCodexInstallation()?.codexVersion ?? null;
@@ -2229,12 +2498,6 @@ function probeReadyWorkerCapabilities(input) {
2229
2498
  maxParallelTasks: input.maxParallelTasks,
2230
2499
  runningTaskCount: 0,
2231
2500
  });
2232
- if (input.pluginVersion) {
2233
- capabilities.pluginVersion = input.pluginVersion;
2234
- for (const agent of capabilities.agents) {
2235
- agent.pluginVersion = input.pluginVersion;
2236
- }
2237
- }
2238
2501
  if (!capabilities.ready) {
2239
2502
  const reasons = Array.isArray(capabilities.degradedReasons) && capabilities.degradedReasons.length > 0
2240
2503
  ? capabilities.degradedReasons.join(', ')
@@ -2272,21 +2535,6 @@ function errorMessage(error) {
2272
2535
  function errorCode(error) {
2273
2536
  return errorMessage(error).split(':')[0]?.trim() || 'unknown_error';
2274
2537
  }
2275
- function readPlaydropPluginVersion(pluginRoot) {
2276
- for (const relativePath of ['.codex-plugin/plugin.json', 'plugin.json', 'package.json']) {
2277
- try {
2278
- const parsed = JSON.parse((0, node_fs_1.readFileSync)(node_path_1.default.join(pluginRoot, relativePath), 'utf8'));
2279
- const version = typeof parsed.version === 'string' ? parsed.version.trim() : '';
2280
- if (version) {
2281
- return version;
2282
- }
2283
- }
2284
- catch {
2285
- continue;
2286
- }
2287
- }
2288
- return null;
2289
- }
2290
2538
  function readWorkerTargetUpdateFailure() {
2291
2539
  const failureFile = node_process_1.default.env.PLAYDROP_WORKER_TARGET_UPDATE_FAILURE_FILE?.trim() ?? '';
2292
2540
  if (!failureFile || !(0, node_fs_1.existsSync)(failureFile)) {
@@ -2303,8 +2551,6 @@ function readWorkerTargetUpdateFailure() {
2303
2551
  writtenAt,
2304
2552
  targetCliVersion: typeof parsed.targetCliVersion === 'string' ? parsed.targetCliVersion : null,
2305
2553
  minimumCliVersion: typeof parsed.minimumCliVersion === 'string' ? parsed.minimumCliVersion : null,
2306
- targetPluginVersion: typeof parsed.targetPluginVersion === 'string' ? parsed.targetPluginVersion : null,
2307
- minimumPluginVersion: typeof parsed.minimumPluginVersion === 'string' ? parsed.minimumPluginVersion : null,
2308
2554
  };
2309
2555
  }
2310
2556
  function collectWorkerLocalStatus() {
@@ -2316,9 +2562,6 @@ function collectWorkerLocalStatus() {
2316
2562
  let cursorVersion = null;
2317
2563
  let npmVersion = null;
2318
2564
  let playwright = null;
2319
- let pluginRoot = null;
2320
- let pluginVersion = null;
2321
- let pluginError = null;
2322
2565
  let targetUpdateFailure = null;
2323
2566
  try {
2324
2567
  codexVersion = probeCodexInstallation()?.codexVersion ?? null;
@@ -2355,14 +2598,6 @@ function collectWorkerLocalStatus() {
2355
2598
  catch (error) {
2356
2599
  degradedReasons.push(errorCode(error));
2357
2600
  }
2358
- try {
2359
- pluginRoot = resolvePlaydropPluginRoot();
2360
- pluginVersion = readPlaydropPluginVersion(pluginRoot);
2361
- }
2362
- catch (error) {
2363
- pluginError = errorMessage(error);
2364
- degradedReasons.push(errorCode(error));
2365
- }
2366
2601
  try {
2367
2602
  targetUpdateFailure = readWorkerTargetUpdateFailure();
2368
2603
  }
@@ -2385,12 +2620,6 @@ function collectWorkerLocalStatus() {
2385
2620
  runningTaskCount: 0,
2386
2621
  maxParallelTasks: (0, runtime_1.readPositiveEnvInt)('PLAYDROP_WORKER_MAX_PARALLEL_TASKS', DEFAULT_WORKER_MAX_PARALLEL_TASKS),
2387
2622
  });
2388
- if (pluginVersion) {
2389
- capabilities.pluginVersion = pluginVersion;
2390
- for (const agent of capabilities.agents) {
2391
- agent.pluginVersion = pluginVersion;
2392
- }
2393
- }
2394
2623
  if (targetUpdateFailure) {
2395
2624
  capabilities.targetUpdateFailure = targetUpdateFailure;
2396
2625
  }
@@ -2402,8 +2631,7 @@ function collectWorkerLocalStatus() {
2402
2631
  degradedReasons.push(errorCode(error));
2403
2632
  }
2404
2633
  const uniqueReasons = Array.from(new Set(degradedReasons));
2405
- const pluginReady = Boolean(pluginRoot && !pluginError);
2406
- const infrastructureReady = Boolean(npmVersion && playwright?.chromiumInstalled && pluginReady);
2634
+ const infrastructureReady = Boolean(npmVersion && playwright?.chromiumInstalled);
2407
2635
  const ready = Boolean(capabilities?.ready) && infrastructureReady;
2408
2636
  return {
2409
2637
  ready,
@@ -2412,12 +2640,6 @@ function collectWorkerLocalStatus() {
2412
2640
  nodeVersion: node_process_1.default.versions.node,
2413
2641
  npmVersion,
2414
2642
  playwright,
2415
- plugin: {
2416
- ready: pluginReady,
2417
- root: pluginRoot,
2418
- version: pluginVersion,
2419
- error: pluginError,
2420
- },
2421
2643
  agents: capabilities?.agents ?? [],
2422
2644
  capabilities,
2423
2645
  setupActions: buildWorkerSetupActions({
@@ -2467,12 +2689,172 @@ function buildWorkerSetupActions(input) {
2467
2689
  }
2468
2690
  return actions;
2469
2691
  }
2470
- function getWorkerStateDir() {
2471
- return node_path_1.default.join(node_os_1.default.homedir(), '.playdrop', 'worker');
2692
+ function resolveWorkerStateDir() {
2693
+ return node_path_1.default.join(resolveWorkerHomeDir(), 'state');
2694
+ }
2695
+ function normalizeWorkerSupervisorType(value) {
2696
+ const normalized = value?.trim().toLowerCase() || 'terminal';
2697
+ if (normalized !== 'app' && normalized !== 'terminal') {
2698
+ throw new Error(`worker_supervisor_type_invalid: expected app or terminal, received ${value ?? ''}.`);
2699
+ }
2700
+ return normalized;
2701
+ }
2702
+ function resolveWorkerSupervisorLockFile(input = {}) {
2703
+ const workerHomeDirectory = input.workerHomeDirectory?.trim();
2704
+ if (workerHomeDirectory && !node_path_1.default.isAbsolute(workerHomeDirectory)) {
2705
+ throw new Error('worker_home_must_be_absolute');
2706
+ }
2707
+ return node_path_1.default.join(workerHomeDirectory || resolveWorkerHomeDir(), 'supervisor.lock');
2708
+ }
2709
+ function parseLinuxProcessStartIdentity(stat) {
2710
+ const commandEnd = stat.lastIndexOf(')');
2711
+ const fieldsAfterCommand = commandEnd === -1
2712
+ ? []
2713
+ : stat.slice(commandEnd + 1).trim().split(/\s+/);
2714
+ const startTimeTicks = fieldsAfterCommand[19];
2715
+ return /^\d+$/.test(startTimeTicks ?? '') ? `linux:${startTimeTicks}` : null;
2716
+ }
2717
+ function readProcessStartIdentity(pid) {
2718
+ if (!Number.isInteger(pid) || pid <= 0) {
2719
+ return null;
2720
+ }
2721
+ if (node_process_1.default.platform === 'linux') {
2722
+ try {
2723
+ return parseLinuxProcessStartIdentity((0, node_fs_1.readFileSync)(`/proc/${pid}/stat`, 'utf8'));
2724
+ }
2725
+ catch {
2726
+ return null;
2727
+ }
2728
+ }
2729
+ if (node_process_1.default.platform === 'darwin') {
2730
+ const result = (0, shellProbe_1.runShell)(`ps -o lstart= -p ${pid}`);
2731
+ const identity = result.output.trim().replace(/\s+/g, ' ');
2732
+ return result.exitCode === 0 && identity ? identity : null;
2733
+ }
2734
+ throw new Error(`worker_supervisor_process_identity_unsupported: ${node_process_1.default.platform}`);
2735
+ }
2736
+ function readWorkerSupervisorLockOwner(file) {
2737
+ let parsed;
2738
+ try {
2739
+ parsed = JSON.parse((0, node_fs_1.readFileSync)(file, 'utf8'));
2740
+ }
2741
+ catch (error) {
2742
+ if (error.code === 'ENOENT') {
2743
+ throw error;
2744
+ }
2745
+ throw new Error(`worker_supervisor_lock_invalid: ${file} does not contain valid owner metadata.`);
2746
+ }
2747
+ if (parsed.schemaVersion !== 1
2748
+ || (parsed.supervisorType !== 'app' && parsed.supervisorType !== 'terminal')
2749
+ || !Number.isInteger(parsed.pid)
2750
+ || (parsed.pid ?? 0) <= 0
2751
+ || typeof parsed.processStartIdentity !== 'string'
2752
+ || !parsed.processStartIdentity.trim()
2753
+ || typeof parsed.workerName !== 'string'
2754
+ || !parsed.workerName.trim()
2755
+ || typeof parsed.createdAt !== 'string'
2756
+ || !parsed.createdAt.trim()) {
2757
+ throw new Error(`worker_supervisor_lock_invalid: ${file} contains incomplete owner metadata.`);
2758
+ }
2759
+ return parsed;
2760
+ }
2761
+ function workerSupervisorOwnerIsLive(owner) {
2762
+ if (!isProcessAlive(owner.pid)) {
2763
+ return false;
2764
+ }
2765
+ const currentIdentity = readProcessStartIdentity(owner.pid);
2766
+ if (!currentIdentity) {
2767
+ throw new Error(`worker_supervisor_process_identity_unavailable: cannot verify PID ${owner.pid}.`);
2768
+ }
2769
+ return currentIdentity === owner.processStartIdentity;
2770
+ }
2771
+ function workerSupervisorHolderDescription(owner) {
2772
+ const supervisor = owner.supervisorType === 'app' ? 'Local Agent app' : 'terminal';
2773
+ return `${supervisor} worker "${owner.workerName}" (PID ${owner.pid})`;
2774
+ }
2775
+ function workerSupervisorOwnersMatch(left, right) {
2776
+ return left.schemaVersion === right.schemaVersion
2777
+ && left.supervisorType === right.supervisorType
2778
+ && left.pid === right.pid
2779
+ && left.processStartIdentity === right.processStartIdentity
2780
+ && left.workerName === right.workerName
2781
+ && left.createdAt === right.createdAt;
2782
+ }
2783
+ function removeStaleWorkerSupervisorLockIfUnchanged(file, observedHolder) {
2784
+ let currentHolder;
2785
+ try {
2786
+ currentHolder = readWorkerSupervisorLockOwner(file);
2787
+ }
2788
+ catch (error) {
2789
+ if (error.code === 'ENOENT') {
2790
+ return false;
2791
+ }
2792
+ throw error;
2793
+ }
2794
+ if (!workerSupervisorOwnersMatch(currentHolder, observedHolder)) {
2795
+ throw new Error(`worker_supervisor_lock_held: ${workerSupervisorHolderDescription(currentHolder)} acquired ${file} during stale-lock recovery.`);
2796
+ }
2797
+ if (workerSupervisorOwnerIsLive(currentHolder)) {
2798
+ throw new Error(`worker_supervisor_lock_held: ${workerSupervisorHolderDescription(currentHolder)} became live during stale-lock recovery.`);
2799
+ }
2800
+ (0, node_fs_1.unlinkSync)(file);
2801
+ return true;
2802
+ }
2803
+ function acquireWorkerSupervisorLock(input) {
2804
+ const file = input.file ?? resolveWorkerSupervisorLockFile();
2805
+ const processStartIdentity = readProcessStartIdentity(node_process_1.default.pid);
2806
+ if (!processStartIdentity) {
2807
+ throw new Error(`worker_supervisor_process_identity_unavailable: cannot identify current PID ${node_process_1.default.pid}.`);
2808
+ }
2809
+ const owner = {
2810
+ schemaVersion: 1,
2811
+ supervisorType: input.supervisorType,
2812
+ pid: node_process_1.default.pid,
2813
+ processStartIdentity,
2814
+ workerName: input.workerName,
2815
+ createdAt: new Date().toISOString(),
2816
+ };
2817
+ (0, node_fs_1.mkdirSync)(node_path_1.default.dirname(file), { recursive: true, mode: 0o700 });
2818
+ (0, node_fs_1.chmodSync)(node_path_1.default.dirname(file), 0o700);
2819
+ for (let attempt = 0; attempt < 2; attempt += 1) {
2820
+ const candidate = `${file}.${node_process_1.default.pid}.${Date.now()}.${attempt}`;
2821
+ (0, node_fs_1.writeFileSync)(candidate, JSON.stringify(owner, null, 2), { encoding: 'utf8', flag: 'wx', mode: 0o600 });
2822
+ try {
2823
+ (0, node_fs_1.linkSync)(candidate, file);
2824
+ return { file, owner };
2825
+ }
2826
+ catch (error) {
2827
+ const code = error.code;
2828
+ if (code !== 'EEXIST') {
2829
+ throw error;
2830
+ }
2831
+ const holder = readWorkerSupervisorLockOwner(file);
2832
+ if (workerSupervisorOwnerIsLive(holder)) {
2833
+ throw new Error(`worker_supervisor_lock_held: ${workerSupervisorHolderDescription(holder)} already owns ${file}. Stop that worker before starting ${input.supervisorType === 'app' ? 'the Local Agent app worker' : 'a terminal worker'}.`);
2834
+ }
2835
+ removeStaleWorkerSupervisorLockIfUnchanged(file, holder);
2836
+ }
2837
+ finally {
2838
+ (0, node_fs_1.rmSync)(candidate, { force: true });
2839
+ }
2840
+ }
2841
+ const holder = readWorkerSupervisorLockOwner(file);
2842
+ throw new Error(`worker_supervisor_lock_held: ${workerSupervisorHolderDescription(holder)} acquired ${file} during stale-lock recovery.`);
2843
+ }
2844
+ function releaseWorkerSupervisorLock(lock) {
2845
+ if (!(0, node_fs_1.existsSync)(lock.file)) {
2846
+ return;
2847
+ }
2848
+ const holder = readWorkerSupervisorLockOwner(lock.file);
2849
+ if (holder.pid !== lock.owner.pid
2850
+ || holder.processStartIdentity !== lock.owner.processStartIdentity) {
2851
+ throw new Error(`worker_supervisor_lock_owner_changed: refusing to remove ${lock.file}.`);
2852
+ }
2853
+ (0, node_fs_1.unlinkSync)(lock.file);
2472
2854
  }
2473
2855
  function workerRuntimeStateFilePath(target) {
2474
2856
  const normalizedTarget = target === 'FIRST_PARTY' ? 'first-party' : 'personal';
2475
- return node_path_1.default.join(getWorkerStateDir(), `${normalizedTarget}-runtime.json`);
2857
+ return node_path_1.default.join(resolveWorkerStateDir(), `${normalizedTarget}-runtime.json`);
2476
2858
  }
2477
2859
  function isProcessAlive(pid) {
2478
2860
  if (!Number.isInteger(pid) || pid <= 0) {
@@ -2515,7 +2897,7 @@ function readActivePersonalWorkerRuntimeState() {
2515
2897
  return parsed;
2516
2898
  }
2517
2899
  function getManagedWorkerPolicyFilePath(env, workerKey) {
2518
- return node_path_1.default.join(getWorkerStateDir(), `update-policy-${env}-${workerKey}.json`);
2900
+ return node_path_1.default.join(resolveWorkerStateDir(), `update-policy-${env}-${workerKey}.json`);
2519
2901
  }
2520
2902
  function writeManagedWorkerUpdatePolicyFile(input) {
2521
2903
  if (!input.updatePolicy) {
@@ -2528,8 +2910,6 @@ function writeManagedWorkerUpdatePolicyFile(input) {
2528
2910
  writtenAt: new Date().toISOString(),
2529
2911
  targetCliVersion: input.updatePolicy.targetCliVersion,
2530
2912
  minimumCliVersion: input.updatePolicy.minimumCliVersion,
2531
- targetPluginVersion: input.updatePolicy.targetPluginVersion,
2532
- minimumPluginVersion: input.updatePolicy.minimumPluginVersion,
2533
2913
  requiredWrapperContractVersion: input.updatePolicy.requiredWrapperContractVersion,
2534
2914
  }, null, 2));
2535
2915
  }
@@ -2549,7 +2929,7 @@ function verifyNpmGlobalInstallPreconditions() {
2549
2929
  return { prefix };
2550
2930
  }
2551
2931
  function managedWorkerWrapperPath(env) {
2552
- return node_path_1.default.join(getWorkerStateDir(), `playdrop-worker-${env}.sh`);
2932
+ return node_path_1.default.join(resolveWorkerStateDir(), `playdrop-worker-${env}.sh`);
2553
2933
  }
2554
2934
  function managedWorkerLaunchAgentLabel(env) {
2555
2935
  return `ai.playdrop.worker.${env}`;
@@ -2677,12 +3057,14 @@ function assertNoSupervisorConflicts(input) {
2677
3057
  async function writeManagedWorkerWrapper(input) {
2678
3058
  const wrapperPath = managedWorkerWrapperPath(input.env);
2679
3059
  const policyFile = getManagedWorkerPolicyFilePath(input.env, input.workerKey);
2680
- const failureFile = node_path_1.default.join(getWorkerStateDir(), `update-failure-${input.env}-${input.workerKey}.json`);
3060
+ const failureFile = node_path_1.default.join(resolveWorkerStateDir(), `update-failure-${input.env}-${input.workerKey}.json`);
3061
+ const workerHomeDir = resolveWorkerHomeDir();
2681
3062
  await (0, promises_1.mkdir)(node_path_1.default.dirname(wrapperPath), { recursive: true });
2682
3063
  const script = `#!/bin/sh
2683
3064
  set -eu
2684
3065
 
2685
- export PATH="${node_path_1.default.join(node_os_1.default.homedir(), '.playdrop', 'worker', 'bin')}:${node_path_1.default.join(node_os_1.default.homedir(), '.local', 'bin')}:/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin:/usr/sbin:/sbin"
3066
+ export PLAYDROP_WORKER_HOME=${shellQuote(workerHomeDir)}
3067
+ export PATH="${node_path_1.default.join(workerHomeDir, 'bin')}:${node_path_1.default.join(node_os_1.default.homedir(), '.local', 'bin')}:/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin:/usr/sbin:/sbin"
2686
3068
  export PLAYDROP_WORKER_WRAPPER_CONTRACT_VERSION="${WORKER_WRAPPER_CONTRACT_VERSION}"
2687
3069
  POLICY_FILE="${policyFile}"
2688
3070
  FAILURE_FILE="${failureFile}"
@@ -2690,20 +3072,16 @@ export PLAYDROP_WORKER_TARGET_UPDATE_FAILURE_FILE="$FAILURE_FILE"
2690
3072
  BACKOFF_SECONDS="\${PLAYDROP_WORKER_UPDATE_FAILURE_BACKOFF_SECONDS:-30}"
2691
3073
  TARGET_CLI_VERSION=""
2692
3074
  MINIMUM_CLI_VERSION=""
2693
- TARGET_PLUGIN_VERSION=""
2694
- MINIMUM_PLUGIN_VERSION=""
2695
3075
  TARGET_UPDATE_FAILED=0
2696
3076
 
2697
3077
  record_failure() {
2698
- node - "$FAILURE_FILE" "$1" "$TARGET_CLI_VERSION" "$MINIMUM_CLI_VERSION" "$TARGET_PLUGIN_VERSION" "$MINIMUM_PLUGIN_VERSION" <<'NODE'
3078
+ node - "$FAILURE_FILE" "$1" "$TARGET_CLI_VERSION" "$MINIMUM_CLI_VERSION" <<'NODE'
2699
3079
  const fs = require("fs");
2700
3080
  const path = require("path");
2701
3081
  const file = process.argv[2];
2702
3082
  const error = process.argv[3] || "worker_update_install_failed";
2703
3083
  const targetCliVersion = process.argv[4] || null;
2704
3084
  const minimumCliVersion = process.argv[5] || null;
2705
- const targetPluginVersion = process.argv[6] || null;
2706
- const minimumPluginVersion = process.argv[7] || null;
2707
3085
  fs.mkdirSync(path.dirname(file), { recursive: true });
2708
3086
  fs.writeFileSync(file, JSON.stringify({
2709
3087
  schemaVersion: 1,
@@ -2711,8 +3089,6 @@ fs.writeFileSync(file, JSON.stringify({
2711
3089
  error,
2712
3090
  targetCliVersion,
2713
3091
  minimumCliVersion,
2714
- targetPluginVersion,
2715
- minimumPluginVersion,
2716
3092
  }, null, 2));
2717
3093
  NODE
2718
3094
  }
@@ -2793,64 +3169,9 @@ process.exit(0);
2793
3169
  NODE
2794
3170
  }
2795
3171
 
2796
- check_plugin_versions() {
2797
- node - "$1" "$2" <<'NODE'
2798
- const childProcess = require("child_process");
2799
- const requiredVersion = String(process.argv[2] || "").trim();
2800
- const mode = String(process.argv[3] || "").trim();
2801
- if (!requiredVersion) {
2802
- process.exit(0);
2803
- }
2804
- function parse(version) {
2805
- const match = version.match(/^(\\d+)\\.(\\d+)\\.(\\d+)(?:[-+][0-9A-Za-z.-]+)?$/);
2806
- return match ? [Number(match[1]), Number(match[2]), Number(match[3])] : null;
2807
- }
2808
- function meetsMinimum(current, minimum) {
2809
- const parsedCurrent = parse(current);
2810
- const parsedMinimum = parse(minimum);
2811
- if (!parsedCurrent || !parsedMinimum) {
2812
- return current === minimum;
2813
- }
2814
- for (let index = 0; index < 3; index += 1) {
2815
- if (parsedCurrent[index] > parsedMinimum[index]) {
2816
- return true;
2817
- }
2818
- if (parsedCurrent[index] < parsedMinimum[index]) {
2819
- return false;
2820
- }
2821
- }
2822
- return true;
2823
- }
2824
- const result = childProcess.spawnSync("playdrop", ["agents", "status", "--json"], { encoding: "utf8" });
2825
- if (result.status !== 0) {
2826
- process.exit(1);
2827
- }
2828
- let parsed;
2829
- try {
2830
- parsed = JSON.parse(result.stdout);
2831
- } catch {
2832
- process.exit(1);
2833
- }
2834
- const agents = Array.isArray(parsed && parsed.agents) ? parsed.agents : [];
2835
- const checked = agents.filter((agent) => agent && agent.id !== "cursor" && agent.cli && agent.cli.installed);
2836
- for (const agent of checked) {
2837
- const version = agent && agent.plugin && typeof agent.plugin.version === "string" ? agent.plugin.version.trim() : "";
2838
- if (mode === "target") {
2839
- if (version !== requiredVersion) {
2840
- process.exit(1);
2841
- }
2842
- } else if (!meetsMinimum(version, requiredVersion)) {
2843
- process.exit(1);
2844
- }
2845
- }
2846
- NODE
2847
- }
2848
-
2849
3172
  validate_policy_file
2850
3173
  TARGET_CLI_VERSION="$(read_policy_value targetCliVersion)"
2851
3174
  MINIMUM_CLI_VERSION="$(read_policy_value minimumCliVersion)"
2852
- TARGET_PLUGIN_VERSION="$(read_policy_value targetPluginVersion)"
2853
- MINIMUM_PLUGIN_VERSION="$(read_policy_value minimumPluginVersion)"
2854
3175
 
2855
3176
  if [ -n "$TARGET_CLI_VERSION" ]; then
2856
3177
  if npm install -g --no-audit --no-fund "@playdrop/playdrop-cli@$TARGET_CLI_VERSION"; then
@@ -2863,25 +3184,11 @@ if [ -n "$TARGET_CLI_VERSION" ]; then
2863
3184
  fi
2864
3185
  fi
2865
3186
 
2866
- if [ -n "$TARGET_PLUGIN_VERSION" ]; then
2867
- if command -v codex >/dev/null 2>&1; then
2868
- playdrop agents update-plugin codex || record_target_failure "worker_update_plugin_install_failed:codex"
2869
- fi
2870
- if command -v claude >/dev/null 2>&1; then
2871
- playdrop agents update-plugin claude || record_target_failure "worker_update_plugin_install_failed:claude"
2872
- fi
2873
- check_plugin_versions "$TARGET_PLUGIN_VERSION" target || record_target_failure "worker_update_plugin_version_mismatch"
2874
- fi
2875
-
2876
3187
  if [ -n "$MINIMUM_CLI_VERSION" ]; then
2877
3188
  CURRENT_CLI_VERSION="$(current_cli_version)" || fail_converge "$?" "worker_update_cli_below_minimum"
2878
3189
  version_meets "$CURRENT_CLI_VERSION" "$MINIMUM_CLI_VERSION" || fail_converge "$?" "worker_update_cli_below_minimum"
2879
3190
  fi
2880
3191
 
2881
- if [ -n "$MINIMUM_PLUGIN_VERSION" ]; then
2882
- check_plugin_versions "$MINIMUM_PLUGIN_VERSION" minimum || fail_converge "$?" "worker_update_plugin_below_minimum"
2883
- fi
2884
-
2885
3192
  if [ "$TARGET_UPDATE_FAILED" -eq 0 ]; then
2886
3193
  rm -f "$FAILURE_FILE"
2887
3194
  fi
@@ -2898,7 +3205,7 @@ async function writeManagedWorkerLaunchAgent(input) {
2898
3205
  }
2899
3206
  const label = managedWorkerLaunchAgentLabel(input.env);
2900
3207
  const plistPath = managedWorkerLaunchAgentPath(input.env);
2901
- const logDir = node_path_1.default.join(getWorkerStateDir(), 'logs');
3208
+ const logDir = node_path_1.default.join(resolveWorkerStateDir(), 'logs');
2902
3209
  assertNoSupervisorConflicts({
2903
3210
  env: input.env,
2904
3211
  workerName: input.workerName,
@@ -2952,34 +3259,6 @@ function launchManagedWorker(label, plistPath) {
2952
3259
  throw new Error(`worker_setup_launchagent_start_failed: launchctl kickstart exited with code ${kickstart.exitCode}. ${kickstart.output.slice(0, 300)}`);
2953
3260
  }
2954
3261
  }
2955
- async function runPluginSetupAction(action, agent) {
2956
- const previousExitCode = node_process_1.default.exitCode;
2957
- node_process_1.default.exitCode = undefined;
2958
- try {
2959
- if (action === 'install') {
2960
- await (0, agents_1.installPlugin)(agent);
2961
- }
2962
- else {
2963
- await (0, agents_1.updatePlugin)(agent);
2964
- }
2965
- if (node_process_1.default.exitCode && node_process_1.default.exitCode !== 0) {
2966
- throw new Error(`worker_setup_plugin_${action}_failed:${agent}`);
2967
- }
2968
- }
2969
- finally {
2970
- node_process_1.default.exitCode = previousExitCode;
2971
- }
2972
- }
2973
- async function ensurePlaydropPluginsForSetup(local) {
2974
- for (const agent of local.agents) {
2975
- if (agent.agent === 'CODEX') {
2976
- await runPluginSetupAction(local.plugin.ready ? 'update' : 'install', 'codex');
2977
- }
2978
- else if (agent.agent === 'CLAUDE_CODE') {
2979
- await runPluginSetupAction(local.plugin.ready ? 'update' : 'install', 'claude');
2980
- }
2981
- }
2982
- }
2983
3262
  async function resolveWorkerStatusPayload(options = {}) {
2984
3263
  loadWorkerEnvFile(options.env);
2985
3264
  const local = collectWorkerLocalStatus();
@@ -3019,6 +3298,7 @@ async function resolveWorkerStatusPayload(options = {}) {
3019
3298
  try {
3020
3299
  const workers = await ctx.client.listMyAgentWorkers();
3021
3300
  server.reachable = true;
3301
+ server.updatePolicy = workers.updatePolicy;
3022
3302
  server.worker = workerKey
3023
3303
  ? workers.workers.find((worker) => worker.workerKey === workerKey) ?? null
3024
3304
  : null;
@@ -3038,7 +3318,6 @@ function printWorkerStatus(payload) {
3038
3318
  console.log(` CLI: ${payload.local.cliVersion}`);
3039
3319
  console.log(` Node: ${payload.local.nodeVersion}`);
3040
3320
  console.log(` npm: ${payload.local.npmVersion ?? 'missing'}`);
3041
- console.log(` PlayDrop plugin: ${payload.local.plugin.ready ? payload.local.plugin.version ?? payload.local.plugin.root ?? 'installed' : 'missing'}`);
3042
3321
  if (payload.local.agents.length > 0) {
3043
3322
  for (const agent of payload.local.agents) {
3044
3323
  console.log(` ${agent.agent}: ${agent.ready ? 'ready' : 'degraded'} ${agent.cliVersion ?? ''}`.trimEnd());
@@ -3098,11 +3377,6 @@ async function setupWorker(options = {}) {
3098
3377
  if (!baseRuntimeReady) {
3099
3378
  throw new Error(`worker_setup_preflight_failed: ${local.degradedReasons.join(', ') || 'local worker runtime is not ready'}`);
3100
3379
  }
3101
- await ensurePlaydropPluginsForSetup(local);
3102
- const postPluginLocal = collectWorkerLocalStatus();
3103
- if (!postPluginLocal.plugin.ready) {
3104
- throw new Error(`worker_setup_plugin_unavailable: ${postPluginLocal.plugin.error ?? 'PlayDrop plugin could not be verified after setup'}`);
3105
- }
3106
3380
  if (target !== 'FIRST_PARTY') {
3107
3381
  (0, output_1.printSuccess)('Personal worker setup complete.', [
3108
3382
  'Run: playdrop worker start --supervise-stdin',
@@ -3311,6 +3585,14 @@ function throwWorkerRuntimeError(error) {
3311
3585
  async function startWorker(options = {}) {
3312
3586
  loadWorkerEnvFile(options.env);
3313
3587
  await (0, commandContext_1.withEnvironment)('worker start', 'Starting the PlayDrop worker', async ({ client, env }) => {
3588
+ const devPluginWorkingTree = resolveDevPluginWorkingTree({
3589
+ workingTree: options.devPluginWorkingTree,
3590
+ env,
3591
+ nodeEnv: node_process_1.default.env.NODE_ENV,
3592
+ });
3593
+ if (devPluginWorkingTree) {
3594
+ console.warn(`[worker] explicit dev plugin working tree enabled: ${devPluginWorkingTree}`);
3595
+ }
3314
3596
  let me;
3315
3597
  try {
3316
3598
  me = await client.me();
@@ -3326,784 +3608,821 @@ async function startWorker(options = {}) {
3326
3608
  throw new Error('worker_session_invalid: the stored session did not resolve to a user. Run "playdrop auth login" and retry.');
3327
3609
  }
3328
3610
  const target = resolveWorkerExecutionTargetFromRole(me.user.role);
3329
- const npmVersion = probeNpmVersion();
3330
- const playwright = probePlaywrightChromium();
3331
- const playdropPluginRoot = resolvePlaydropPluginRoot();
3332
- const playdropPluginVersion = readPlaydropPluginVersion(playdropPluginRoot);
3333
- const workerKey = (0, config_1.getOrCreateWorkerKey)();
3611
+ const initialWorkerPolicy = (await client.listMyAgentWorkers()).updatePolicy;
3612
+ if (initialWorkerPolicy.executionTarget !== target) {
3613
+ throw new Error(`worker_update_policy_target_mismatch: expected ${target}, received ${initialWorkerPolicy.executionTarget}.`);
3614
+ }
3334
3615
  const workerName = options.name?.trim() || `${username} - ${node_os_1.default.hostname()}`;
3335
- const maxParallelTasks = (0, runtime_1.readPositiveEnvInt)('PLAYDROP_WORKER_MAX_PARALLEL_TASKS', DEFAULT_WORKER_MAX_PARALLEL_TASKS);
3336
- let capabilities = probeReadyWorkerCapabilities({
3337
- npmVersion,
3338
- playwright,
3339
- maxParallelTasks,
3340
- pluginVersion: playdropPluginVersion,
3341
- });
3342
- let lastCapabilityRefreshAt = Date.now();
3343
- writeWorkerRuntimeState({
3344
- env,
3345
- workerKey,
3346
- workerName,
3347
- target,
3348
- });
3349
- const activeTaskIds = new Set();
3350
- const activeTaskRuns = new Map();
3351
- const activeTerminators = new Map();
3352
- const activeDevPorts = new Set();
3353
- const workerDevPortBase = (0, runtime_1.readPositiveEnvInt)('PLAYDROP_WORKER_DEV_PORT_BASE', DEFAULT_WORKER_DEV_PORT_BASE);
3354
- let sessionExpired = false;
3355
- let shuttingDown = false;
3356
- let quiescingForUpdate = false;
3357
- let shutdownSignal = null;
3358
- let crashed = false;
3359
- let fatalTaskError = null;
3360
- let presenceTimer = null;
3361
- const firstActiveTaskId = () => activeTaskIds.values().next().value ?? null;
3362
- const terminateActiveTasks = () => {
3363
- for (const terminate of activeTerminators.values()) {
3364
- terminate();
3365
- }
3366
- };
3367
- const currentCapabilities = () => ({
3368
- ...capabilities,
3369
- runningTaskCount: activeTaskRuns.size,
3370
- });
3371
- const presenceBody = () => ({
3372
- workerKey,
3373
- name: workerName,
3374
- environment: env,
3375
- capabilities: currentCapabilities(),
3376
- lifecycleState: quiescingForUpdate ? 'QUIESCING' : 'READY',
3377
- updateState: quiescingForUpdate ? 'UPDATE_REQUIRED' : 'CURRENT',
3378
- ready: capabilities.ready,
3379
- activeTaskCount: activeTaskRuns.size,
3380
- cliVersion: (0, clientInfo_1.getCliVersion)(),
3381
- pluginVersion: capabilities.pluginVersion,
3382
- });
3383
- const recordFatalTaskError = (error) => {
3384
- if (!fatalTaskError) {
3385
- fatalTaskError = error instanceof Error ? error : new Error(String(error));
3386
- }
3387
- shuttingDown = true;
3388
- terminateActiveTasks();
3389
- };
3390
- const handleWorkerAction = (response) => {
3391
- const requiredWrapperVersion = response?.updatePolicy?.requiredWrapperContractVersion;
3392
- if (typeof requiredWrapperVersion === 'number'
3393
- && requiredWrapperVersion > WORKER_WRAPPER_CONTRACT_VERSION) {
3394
- recordFatalTaskError(new Error(`worker_wrapper_update_required: run "playdrop worker setup --env ${env}" to install wrapper contract ${requiredWrapperVersion}.`));
3395
- return;
3396
- }
3397
- if (response?.action !== 'quiesce_for_update' || quiescingForUpdate) {
3398
- return;
3399
- }
3400
- writeManagedWorkerUpdatePolicyFile({
3401
- env,
3616
+ const supervisorType = normalizeWorkerSupervisorType(options.supervisor);
3617
+ const supervisorLock = acquireWorkerSupervisorLock({ supervisorType, workerName });
3618
+ try {
3619
+ await removeStaleAgentRunCredentials();
3620
+ const npmVersion = probeNpmVersion();
3621
+ const playwright = probePlaywrightChromium();
3622
+ const workerKey = (0, config_1.getOrCreateWorkerKey)();
3623
+ const maxParallelTasks = (0, runtime_1.readPositiveEnvInt)('PLAYDROP_WORKER_MAX_PARALLEL_TASKS', DEFAULT_WORKER_MAX_PARALLEL_TASKS);
3624
+ let capabilities = probeReadyWorkerCapabilities({
3625
+ npmVersion,
3626
+ playwright,
3627
+ maxParallelTasks,
3628
+ });
3629
+ let lastCapabilityRefreshAt = Date.now();
3630
+ writeWorkerRuntimeState({
3631
+ env,
3402
3632
  workerKey,
3403
- updatePolicy: response.updatePolicy,
3633
+ workerName,
3634
+ target,
3404
3635
  });
3405
- quiescingForUpdate = true;
3406
- console.log('Worker entering QUIESCING for managed update. No new tasks will be claimed.');
3407
- };
3408
- const signalHandler = (signal) => {
3409
- shutdownSignal = signal;
3410
- if (shuttingDown) {
3411
- return;
3412
- }
3413
- shuttingDown = true;
3414
- // Task runs finish their own lifecycle (flush, fail, clean) before exit.
3415
- terminateActiveTasks();
3416
- };
3417
- const stdinShutdownHandler = () => {
3418
- shutdownSignal = 'SIGTERM';
3419
- if (shuttingDown) {
3420
- return;
3421
- }
3422
- shuttingDown = true;
3423
- terminateActiveTasks();
3424
- };
3425
- node_process_1.default.once('SIGINT', signalHandler);
3426
- node_process_1.default.once('SIGTERM', signalHandler);
3427
- if (options.superviseStdin) {
3428
- node_process_1.default.stdin.once('end', stdinShutdownHandler);
3429
- node_process_1.default.stdin.once('close', stdinShutdownHandler);
3430
- node_process_1.default.stdin.resume();
3431
- }
3432
- console.log(`PlayDrop Worker started as "${workerName}" (${target}).`);
3433
- await sendWorkerHealthAlertSafely({ state: 'started', env, workerName });
3434
- // Marks the session expired, kills any in-flight agent child, and lets the
3435
- // run loop finish the task lifecycle before the worker stops non-zero.
3436
- const handleSessionExpiry = () => {
3437
- if (sessionExpired) {
3438
- return;
3439
- }
3440
- sessionExpired = true;
3441
- console.error(exports.WORKER_SESSION_EXPIRED_MESSAGE);
3442
- terminateActiveTasks();
3443
- };
3444
- try {
3445
- handleWorkerAction(await client.workerPresence(presenceBody()));
3446
- }
3447
- catch (error) {
3448
- if (isWorkerAuthFailureError(error)) {
3449
- throw new Error(exports.WORKER_SESSION_EXPIRED_MESSAGE);
3450
- }
3451
- throw error;
3452
- }
3453
- presenceTimer = setInterval(() => {
3454
- client.workerPresence(presenceBody()).then(handleWorkerAction).catch((error) => {
3455
- if (isWorkerAuthFailureError(error)) {
3456
- handleSessionExpiry();
3457
- return;
3636
+ const activeTaskIds = new Set();
3637
+ const activeTaskRuns = new Map();
3638
+ const activeTerminators = new Map();
3639
+ const activeDevPorts = new Set();
3640
+ const workerDevPortBase = (0, runtime_1.readPositiveEnvInt)('PLAYDROP_WORKER_DEV_PORT_BASE', DEFAULT_WORKER_DEV_PORT_BASE);
3641
+ let sessionExpired = false;
3642
+ let shuttingDown = false;
3643
+ let quiescingForUpdate = false;
3644
+ let shutdownSignal = null;
3645
+ let crashed = false;
3646
+ let fatalTaskError = null;
3647
+ let presenceTimer = null;
3648
+ let currentWorkerPolicy = initialWorkerPolicy;
3649
+ const firstActiveTaskId = () => activeTaskIds.values().next().value ?? null;
3650
+ const terminateActiveTasks = () => {
3651
+ for (const terminate of activeTerminators.values()) {
3652
+ terminate();
3458
3653
  }
3459
- console.error(`Worker presence heartbeat failed: ${error instanceof Error ? error.message : String(error)}`);
3654
+ };
3655
+ const currentCapabilities = () => ({
3656
+ ...capabilities,
3657
+ runningTaskCount: activeTaskRuns.size,
3460
3658
  });
3461
- }, HEARTBEAT_INTERVAL_MS);
3462
- const runClaimedTask = async (claim, devPort) => {
3463
- const task = claim.task;
3464
- if (!task) {
3465
- throw new Error('agent_task_claim_missing_task');
3466
- }
3467
- const leaseToken = claim.leaseToken?.trim();
3468
- if (!leaseToken) {
3469
- throw new Error('agent_task_claim_missing_lease_token');
3470
- }
3471
- // The lease token lives only in supervisor memory, never in the task
3472
- // workspace, on-disk worker state, or the agent child environment.
3473
- let heartbeatTimer = null;
3474
- let eventDrainTimer = null;
3475
- let eventDrainPromise = Promise.resolve();
3476
- let fenced = false;
3477
- let heartbeatObservedFinalizedTask = false;
3478
- let retainWorkspace = false;
3479
- let workspaceDir = null;
3480
- let availableSkillPaths = [];
3481
- const observedSkillPaths = new Set();
3482
- let assignment = null;
3483
- let resolvedRuntimeModel = task.model;
3484
- let reasoningEffort = null;
3485
- let agentStartedAt = null;
3486
- let agentCompletedAt = null;
3487
- let agentResult = null;
3488
- let telemetryReported = false;
3489
- let heartbeatTransientFailures = 0;
3490
- const reportTelemetry = async (status, setupError) => {
3491
- if (!assignment) {
3659
+ const presenceBody = () => ({
3660
+ workerKey,
3661
+ name: workerName,
3662
+ environment: env,
3663
+ capabilities: currentCapabilities(),
3664
+ lifecycleState: quiescingForUpdate ? 'QUIESCING' : 'READY',
3665
+ updateState: quiescingForUpdate ? 'UPDATE_REQUIRED' : 'CURRENT',
3666
+ ready: !quiescingForUpdate && capabilities.ready,
3667
+ activeTaskCount: activeTaskRuns.size,
3668
+ cliVersion: (0, clientInfo_1.getCliVersion)(),
3669
+ targetCliVersion: currentWorkerPolicy.targetCliVersion,
3670
+ });
3671
+ const recordFatalTaskError = (error) => {
3672
+ if (!fatalTaskError) {
3673
+ fatalTaskError = error instanceof Error ? error : new Error(String(error));
3674
+ }
3675
+ shuttingDown = true;
3676
+ terminateActiveTasks();
3677
+ };
3678
+ const handleWorkerAction = (response) => {
3679
+ if (response?.updatePolicy) {
3680
+ if (response.updatePolicy.executionTarget !== target) {
3681
+ recordFatalTaskError(new Error(`worker_update_policy_target_mismatch: expected ${target}, received ${response.updatePolicy.executionTarget}.`));
3682
+ return;
3683
+ }
3684
+ currentWorkerPolicy = response.updatePolicy;
3685
+ }
3686
+ const requiredWrapperVersion = response?.updatePolicy?.requiredWrapperContractVersion;
3687
+ if (typeof requiredWrapperVersion === 'number'
3688
+ && requiredWrapperVersion > WORKER_WRAPPER_CONTRACT_VERSION) {
3689
+ recordFatalTaskError(new Error(`worker_wrapper_update_required: run "playdrop worker setup --env ${env}" to install wrapper contract ${requiredWrapperVersion}.`));
3492
3690
  return;
3493
3691
  }
3494
- const completedAt = agentCompletedAt ?? new Date();
3495
- await recordAgentRunTelemetry({
3496
- client,
3497
- task,
3498
- assignment,
3692
+ if (response?.action !== 'quiesce_for_update' || quiescingForUpdate) {
3693
+ return;
3694
+ }
3695
+ writeManagedWorkerUpdatePolicyFile({
3696
+ env,
3499
3697
  workerKey,
3500
- leaseToken,
3501
- result: agentResult ?? buildSupervisorFailureRunResult(setupError ?? 'worker_setup_failed'),
3502
- status,
3503
- resolvedRuntimeModel,
3504
- reasoningEffort,
3505
- availableSkillPaths,
3506
- observedSkillPaths: [...observedSkillPaths],
3507
- startedAt: agentStartedAt ?? completedAt,
3508
- completedAt,
3698
+ updatePolicy: response.updatePolicy,
3509
3699
  });
3510
- telemetryReported = true;
3700
+ quiescingForUpdate = true;
3701
+ console.log('Worker entering QUIESCING for managed update. No new tasks will be claimed.');
3702
+ };
3703
+ const signalHandler = (signal) => {
3704
+ shutdownSignal = signal;
3705
+ if (shuttingDown) {
3706
+ return;
3707
+ }
3708
+ shuttingDown = true;
3709
+ // Task runs finish their own lifecycle (flush, fail, clean) before exit.
3710
+ terminateActiveTasks();
3711
+ };
3712
+ const stdinShutdownHandler = () => {
3713
+ shutdownSignal = 'SIGTERM';
3714
+ if (shuttingDown) {
3715
+ return;
3716
+ }
3717
+ shuttingDown = true;
3718
+ terminateActiveTasks();
3719
+ };
3720
+ node_process_1.default.once('SIGINT', signalHandler);
3721
+ node_process_1.default.once('SIGTERM', signalHandler);
3722
+ if (options.superviseStdin) {
3723
+ node_process_1.default.stdin.once('end', stdinShutdownHandler);
3724
+ node_process_1.default.stdin.once('close', stdinShutdownHandler);
3725
+ node_process_1.default.stdin.resume();
3726
+ }
3727
+ console.log(`PlayDrop Worker started as "${workerName}" (${target}).`);
3728
+ await sendWorkerHealthAlertSafely({ state: 'started', env, workerName });
3729
+ // Marks the session expired, kills any in-flight agent child, and lets the
3730
+ // run loop finish the task lifecycle before the worker stops non-zero.
3731
+ const handleSessionExpiry = () => {
3732
+ if (sessionExpired) {
3733
+ return;
3734
+ }
3735
+ sessionExpired = true;
3736
+ console.error(exports.WORKER_SESSION_EXPIRED_MESSAGE);
3737
+ terminateActiveTasks();
3511
3738
  };
3512
3739
  try {
3513
- assignment = resolveWorkerClaimTaskAssignment(claim);
3514
- if (!assignment) {
3515
- throw new Error('agent_task_claim_missing_task_assignment');
3740
+ handleWorkerAction(await client.workerPresence(presenceBody()));
3741
+ }
3742
+ catch (error) {
3743
+ if (isWorkerAuthFailureError(error)) {
3744
+ throw new Error(exports.WORKER_SESSION_EXPIRED_MESSAGE);
3516
3745
  }
3517
- const codexModel = assignment.agent === 'CODEX' ? resolveCodexModel(assignment.model) : null;
3518
- const claudeModel = assignment.agent === 'CLAUDE_CODE' ? resolveClaudeModel(assignment.model) : null;
3519
- resolvedRuntimeModel = codexModel?.model ?? claudeModel?.model ?? assignment.model;
3520
- reasoningEffort = codexModel?.reasoningEffort ?? claudeModel?.effort ?? null;
3521
- const taskContext = buildWorkerTaskContext(claim, assignment);
3522
- workspaceDir = workerTaskWorkspaceDirFromAssignment(assignment);
3523
- await (0, promises_1.rm)(workspaceDir, { recursive: true, force: true });
3524
- await (0, promises_1.mkdir)(workspaceDir, { recursive: true });
3525
- const eventDir = node_path_1.default.join(workspaceDir, '.playdrop-task-events');
3526
- await (0, promises_1.mkdir)(eventDir, { recursive: true });
3527
- await stageWorkerTaskContext({
3528
- workspaceDir,
3529
- env,
3530
- devPort,
3531
- taskContext,
3532
- });
3533
- const binDir = await createLocalPlaydropShim({
3534
- workspaceDir,
3535
- taskId: taskContext.taskId,
3536
- attempt: taskContext.attempt,
3537
- devPort,
3538
- });
3539
- const handleEventDrainFailure = (error) => {
3540
- const classification = classifyWorkerEventDrainError(error);
3541
- if (classification === 'session_expired') {
3746
+ throw error;
3747
+ }
3748
+ presenceTimer = setInterval(() => {
3749
+ client.workerPresence(presenceBody()).then(handleWorkerAction).catch((error) => {
3750
+ if (isWorkerAuthFailureError(error)) {
3542
3751
  handleSessionExpiry();
3543
3752
  return;
3544
3753
  }
3545
- if (classification === 'lease_invalid') {
3546
- fenced = true;
3547
- activeTerminators.get(task.id)?.();
3548
- return;
3549
- }
3550
- console.error(`Agent task event drain failed: ${error instanceof Error ? error.message : String(error)}`);
3551
- };
3552
- const queueEventDrain = () => {
3553
- eventDrainPromise = eventDrainPromise.then(() => drainWorkerEventQueue({ eventDir, client, taskId: task.id, workerKey, leaseToken }), () => drainWorkerEventQueue({ eventDir, client, taskId: task.id, workerKey, leaseToken }));
3554
- return eventDrainPromise;
3555
- };
3556
- const appendObservedSkillPathsFromTranscript = (chunks) => {
3557
- if (availableSkillPaths.length <= 0) {
3754
+ console.error(`Worker presence heartbeat failed: ${error instanceof Error ? error.message : String(error)}`);
3755
+ });
3756
+ }, HEARTBEAT_INTERVAL_MS);
3757
+ const runClaimedTask = async (claim, devPort) => {
3758
+ const task = claim.task;
3759
+ if (!task) {
3760
+ throw new Error('agent_task_claim_missing_task');
3761
+ }
3762
+ const leaseToken = claim.leaseToken?.trim();
3763
+ if (!leaseToken) {
3764
+ throw new Error('agent_task_claim_missing_lease_token');
3765
+ }
3766
+ // The lease token lives only in supervisor memory, never in the task
3767
+ // workspace, on-disk worker state, or the agent child environment.
3768
+ let heartbeatTimer = null;
3769
+ let eventDrainTimer = null;
3770
+ let eventDrainPromise = Promise.resolve();
3771
+ let fenced = false;
3772
+ let heartbeatObservedFinalizedTask = false;
3773
+ let retainWorkspace = false;
3774
+ let workspaceDir = null;
3775
+ let availableSkillPaths = [];
3776
+ const observedSkillPaths = new Set();
3777
+ let assignment = null;
3778
+ let resolvedRuntimeModel = task.model;
3779
+ let reasoningEffort = null;
3780
+ let agentStartedAt = null;
3781
+ let agentCompletedAt = null;
3782
+ let agentResult = null;
3783
+ let telemetryReported = false;
3784
+ let heartbeatTransientFailures = 0;
3785
+ const reportTelemetry = async (status, setupError) => {
3786
+ if (!assignment) {
3558
3787
  return;
3559
3788
  }
3560
- for (const chunk of chunks) {
3561
- for (const observedPath of collectObservedPlaydropSkillPaths(chunk.content, availableSkillPaths)) {
3562
- observedSkillPaths.add(observedPath);
3563
- }
3564
- }
3565
- };
3566
- const appendTranscriptChunks = async (chunks) => {
3567
- appendObservedSkillPathsFromTranscript(chunks);
3568
- await appendWorkerTranscriptChunks({ client, taskId: task.id, workerKey, leaseToken, chunks });
3569
- };
3570
- await client.workerCreateAgentTaskEvent(task.id, {
3571
- workerKey,
3572
- leaseToken,
3573
- kind: 'progress',
3574
- phase: 'setup',
3575
- message: 'Preparing worker workspace',
3576
- pct: 2,
3577
- });
3578
- const assignmentPluginRoot = assignment.pluginBundle
3579
- ? await stageAssignmentPluginBundle({ workspaceDir, pluginBundle: assignment.pluginBundle })
3580
- : playdropPluginRoot;
3581
- availableSkillPaths = normalizeTelemetrySkillPaths(await stagePlaydropPluginReferences({ workspaceDir, pluginRoot: assignmentPluginRoot, kind: task.kind }));
3582
- await client.workerCreateAgentTaskEvent(task.id, {
3583
- workerKey,
3584
- leaseToken,
3585
- kind: 'progress',
3586
- phase: 'setup',
3587
- message: 'Staged PlayDrop plugin references',
3588
- pct: 3,
3589
- });
3590
- if (assignment.workspace.files.length > 0) {
3591
- await client.workerCreateAgentTaskEvent(task.id, {
3789
+ const completedAt = agentCompletedAt ?? new Date();
3790
+ await recordAgentRunTelemetry({
3791
+ client,
3792
+ task,
3793
+ assignment,
3592
3794
  workerKey,
3593
3795
  leaseToken,
3594
- kind: 'progress',
3595
- phase: 'setup',
3596
- message: `Staging ${assignment.workspace.files.length} server-provided workspace file${assignment.workspace.files.length === 1 ? '' : 's'}`,
3597
- pct: 3,
3796
+ result: agentResult ?? buildSupervisorFailureRunResult(setupError ?? 'worker_setup_failed'),
3797
+ status,
3798
+ resolvedRuntimeModel,
3799
+ reasoningEffort,
3800
+ availableSkillPaths,
3801
+ observedSkillPaths: [...observedSkillPaths],
3802
+ startedAt: agentStartedAt ?? completedAt,
3803
+ completedAt,
3598
3804
  });
3599
- await stageAssignmentWorkspaceFiles(workspaceDir, assignment.workspace.files);
3600
- }
3601
- heartbeatTimer = setInterval(() => {
3602
- client.workerHeartbeatAgentTask(task.id, { workerKey, leaseToken })
3603
- .then((heartbeat) => {
3604
- heartbeatTransientFailures = 0;
3605
- if (heartbeat.action === 'abort') {
3606
- fenced = true;
3607
- activeTerminators.get(task.id)?.();
3608
- }
3609
- })
3610
- .catch((error) => {
3611
- const classification = classifyWorkerHeartbeatError(error);
3805
+ telemetryReported = true;
3806
+ };
3807
+ try {
3808
+ assignment = resolveWorkerClaimTaskAssignment(claim);
3809
+ if (!assignment) {
3810
+ throw new Error('agent_task_claim_missing_task_assignment');
3811
+ }
3812
+ const codexModel = assignment.agent === 'CODEX' ? resolveCodexModel(assignment.model) : null;
3813
+ const claudeModel = assignment.agent === 'CLAUDE_CODE' ? resolveClaudeModel(assignment.model) : null;
3814
+ resolvedRuntimeModel = codexModel?.model ?? claudeModel?.model ?? assignment.model;
3815
+ reasoningEffort = codexModel?.reasoningEffort ?? claudeModel?.effort ?? null;
3816
+ const taskContext = buildWorkerTaskContext(claim, assignment);
3817
+ workspaceDir = workerTaskWorkspaceDirFromAssignment(assignment);
3818
+ await (0, promises_1.rm)(workspaceDir, { recursive: true, force: true });
3819
+ await (0, promises_1.mkdir)(workspaceDir, { recursive: true });
3820
+ const eventDir = node_path_1.default.join(workspaceDir, '.playdrop-task-events');
3821
+ await (0, promises_1.mkdir)(eventDir, { recursive: true });
3822
+ await stageWorkerTaskContext({
3823
+ workspaceDir,
3824
+ env,
3825
+ devPort,
3826
+ taskContext,
3827
+ });
3828
+ const binDir = await createLocalPlaydropShim({
3829
+ workspaceDir,
3830
+ taskId: taskContext.taskId,
3831
+ attempt: taskContext.attempt,
3832
+ devPort,
3833
+ });
3834
+ const handleEventDrainFailure = (error) => {
3835
+ const classification = classifyWorkerEventDrainError(error);
3612
3836
  if (classification === 'session_expired') {
3613
3837
  handleSessionExpiry();
3614
3838
  return;
3615
3839
  }
3616
3840
  if (classification === 'lease_invalid') {
3617
- void (async () => {
3618
- const refreshed = await fetchTaskDetail(client, target, task.id).catch(() => null);
3619
- if (refreshed?.task.status === 'DONE') {
3620
- fenced = true;
3621
- if (!heartbeatObservedFinalizedTask) {
3622
- heartbeatObservedFinalizedTask = true;
3623
- console.error(`Agent task ${task.id} lease ended after task completed as DONE; waiting for agent exit.`);
3624
- }
3625
- return;
3626
- }
3627
- if (refreshed && refreshed.task.status !== 'RUNNING' && refreshed.task.status !== 'QUEUED') {
3628
- console.error(`Agent task ${task.id} lease ended after task finalized as ${refreshed.task.status}; terminating agent child.`);
3629
- }
3630
- fenced = true;
3631
- activeTerminators.get(task.id)?.();
3632
- })().catch((heartbeatError) => {
3633
- fenced = true;
3634
- activeTerminators.get(task.id)?.();
3635
- console.error(`Agent task lease check failed: ${heartbeatError instanceof Error ? heartbeatError.message : String(heartbeatError)}`);
3636
- });
3841
+ fenced = true;
3842
+ activeTerminators.get(task.id)?.();
3637
3843
  return;
3638
3844
  }
3639
- if (classification === 'transient_network') {
3640
- heartbeatTransientFailures += 1;
3641
- console.error(`Agent task heartbeat transient failure ${heartbeatTransientFailures}/${TASK_HEARTBEAT_TRANSIENT_FAILURE_LIMIT}: ${error instanceof Error ? error.message : String(error)}`);
3642
- if (heartbeatTransientFailures >= TASK_HEARTBEAT_TRANSIENT_FAILURE_LIMIT) {
3643
- fenced = true;
3644
- activeTerminators.get(task.id)?.();
3645
- console.error(`Agent task ${task.id} heartbeat could not reach the API; terminating agent child.`);
3646
- }
3845
+ console.error(`Agent task event drain failed: ${error instanceof Error ? error.message : String(error)}`);
3846
+ };
3847
+ const queueEventDrain = () => {
3848
+ eventDrainPromise = eventDrainPromise.then(() => drainWorkerEventQueue({ eventDir, client, taskId: task.id, workerKey, leaseToken }), () => drainWorkerEventQueue({ eventDir, client, taskId: task.id, workerKey, leaseToken }));
3849
+ return eventDrainPromise;
3850
+ };
3851
+ const appendObservedSkillPathsFromTranscript = (chunks) => {
3852
+ if (availableSkillPaths.length <= 0) {
3647
3853
  return;
3648
3854
  }
3649
- console.error(`Agent task heartbeat failed: ${error instanceof Error ? error.message : String(error)}`);
3650
- });
3651
- }, task.kind === 'GAME_REVIEW' || task.kind === 'GAME_EVAL'
3652
- ? INSTRUMENT_HEARTBEAT_INTERVAL_MS
3653
- : HEARTBEAT_INTERVAL_MS);
3654
- eventDrainTimer = setInterval(() => {
3655
- queueEventDrain().catch((error) => {
3656
- handleEventDrainFailure(error);
3657
- });
3658
- }, 1000);
3659
- const prompt = assignment.prompt;
3660
- if (task.kind === 'GAME_UPDATE') {
3661
- const baseSourcePayload = assignment.inputs.baseSource;
3662
- const baseSource = resolveWorkerBaseSource(baseSourcePayload);
3855
+ for (const chunk of chunks) {
3856
+ for (const observedPath of collectObservedPlaydropSkillPaths(chunk.content, availableSkillPaths)) {
3857
+ observedSkillPaths.add(observedPath);
3858
+ }
3859
+ }
3860
+ };
3861
+ const appendTranscriptChunks = async (chunks) => {
3862
+ appendObservedSkillPathsFromTranscript(chunks);
3863
+ await appendWorkerTranscriptChunks({ client, taskId: task.id, workerKey, leaseToken, chunks });
3864
+ };
3663
3865
  await client.workerCreateAgentTaskEvent(task.id, {
3664
3866
  workerKey,
3665
3867
  leaseToken,
3666
3868
  kind: 'progress',
3667
3869
  phase: 'setup',
3668
- message: `Staging base source ${baseSource.appName} version ${baseSource.version} into the workspace`,
3669
- pct: 4,
3870
+ message: 'Preparing worker workspace',
3871
+ pct: 2,
3670
3872
  });
3671
- await stageWorkerBaseAppSource({
3672
- client,
3873
+ const assignmentPluginRoot = devPluginWorkingTree ?? await stageAssignmentPluginBundle({
3673
3874
  workspaceDir,
3674
- creatorUsername: baseSourcePayload.creatorUsername,
3675
- baseApp: baseSource,
3875
+ pluginBundle: assignment.pluginBundle,
3676
3876
  });
3677
- }
3678
- else if (task.kind === 'NEW_GAME' || task.kind === 'REMIX_GAME') {
3679
- if (assignment.inputs.baseSource !== null) {
3680
- throw new Error('agent_task_assignment_unexpected_base_source');
3681
- }
3682
- if (task.kind === 'REMIX_GAME' && !assignment.inputs.remixSource) {
3683
- throw new Error('agent_task_assignment_remix_source_missing');
3684
- }
3685
- }
3686
- else if (task.kind === 'GAME_REVIEW') {
3687
- if (assignment.inputs.baseSource !== null) {
3688
- throw new Error('agent_task_assignment_unexpected_base_source');
3689
- }
3690
- if (!assignment.inputs.reviewTarget) {
3691
- throw new Error('agent_task_assignment_review_target_missing');
3692
- }
3693
- }
3694
- else if (task.kind === 'GAME_EVAL') {
3695
- if (assignment.inputs.baseSource !== null) {
3696
- throw new Error('agent_task_assignment_unexpected_base_source');
3697
- }
3698
- if (!assignment.inputs.evalTarget) {
3699
- throw new Error('agent_task_assignment_eval_target_missing');
3700
- }
3701
- }
3702
- else {
3703
- throw new Error(`unsupported_agent_task_kind:${task.kind}`);
3704
- }
3705
- if (shuttingDown) {
3706
- throw new Error('worker_shutdown');
3707
- }
3708
- agentStartedAt = new Date();
3709
- if (assignment.agent === 'CODEX') {
3710
- agentResult = await runCodex({
3877
+ const stagedPluginPaths = await stagePlaydropPluginReferences({
3711
3878
  workspaceDir,
3712
- binDir,
3713
- eventDir,
3714
- prompt,
3715
- envName: env,
3716
- taskId: task.id,
3717
- attempt: taskContext.attempt,
3718
- devPort,
3719
- codexModel: codexModel ?? resolveCodexModel(assignment.model),
3720
- onTranscriptChunks: async (chunks) => {
3721
- await appendTranscriptChunks(chunks);
3722
- },
3723
- onChild: (controls) => {
3724
- activeTerminators.set(task.id, controls.terminate);
3725
- },
3726
- });
3727
- }
3728
- else if (assignment.agent === 'CLAUDE_CODE') {
3729
- agentResult = await runClaude({
3730
- workspaceDir,
3731
- binDir,
3732
- eventDir,
3733
- prompt,
3734
- enableChrome: task.kind === 'GAME_REVIEW' || task.kind === 'GAME_EVAL',
3735
- envName: env,
3736
- taskId: task.id,
3737
- attempt: taskContext.attempt,
3738
- devPort,
3739
- claudeModel: claudeModel ?? resolveClaudeModel(assignment.model),
3740
- playdropPluginRoot: assignmentPluginRoot,
3741
- onTranscriptChunks: async (chunks) => {
3742
- await appendTranscriptChunks(chunks);
3743
- },
3744
- onChild: (controls) => {
3745
- activeTerminators.set(task.id, controls.terminate);
3746
- },
3747
- });
3748
- }
3749
- else if (assignment.agent === 'CURSOR_COMPOSER') {
3750
- const assignmentAgent = assignment.agent;
3751
- const assignmentModel = assignment.model;
3752
- const plannedAttemptIndex = assignment.attemptPlan?.options.findIndex((option) => option.agent === assignmentAgent && option.model === assignmentModel);
3753
- const attemptIndex = typeof plannedAttemptIndex === 'number' && plannedAttemptIndex >= 0
3754
- ? plannedAttemptIndex
3755
- : 0;
3756
- await client.workerRecordAgentTaskAttemptUnavailable(task.id, {
3757
- workerKey,
3758
- leaseToken,
3759
- attemptIndex,
3760
- reason: 'unavailable_runtime',
3761
- message: 'Cursor Composer noninteractive worker execution is not configured on this worker.',
3879
+ pluginRoot: assignmentPluginRoot,
3880
+ kind: task.kind,
3762
3881
  });
3763
- fenced = true;
3764
- throw new Error('worker_cursor_composer_runner_not_configured');
3765
- }
3766
- else {
3767
- throw new Error(`unsupported_worker_agent:${assignment.agent}`);
3768
- }
3769
- agentCompletedAt = new Date();
3770
- await queueEventDrain().catch((error) => {
3771
- handleEventDrainFailure(error);
3772
- });
3773
- const completedAgentResult = agentResult;
3774
- const agentRunResult = buildAgentRunResult(completedAgentResult);
3775
- if (fenced) {
3776
- const refreshed = await fetchTaskDetail(client, target, task.id).catch(() => null);
3777
- if (refreshed && refreshed.task.status !== 'RUNNING' && refreshed.task.status !== 'QUEUED') {
3778
- retainWorkspace = refreshed.task.status === 'FAILED';
3779
- console.error(`Agent task ${task.id} was finalized by the server as ${refreshed.task.status}.`);
3780
- await reportTelemetry(refreshed.task.status);
3781
- }
3782
- else {
3783
- console.error(`Agent task ${task.id} was aborted by the server; cleaning up without reporting failure.`);
3882
+ if (assignment.agent === 'CODEX') {
3883
+ await stageCodexTaskSkills({ workspaceDir, stagedPluginPaths });
3784
3884
  }
3785
- }
3786
- else if (sessionExpired) {
3787
- // Best effort: the session is already invalid, so this fail call
3788
- // likely 401s too; failTaskWithRetry logs loudly after the retry.
3789
- await failTaskWithRetry(client, task.id, {
3790
- workerKey,
3791
- leaseToken,
3792
- error: 'worker_session_expired',
3793
- result: agentRunResult,
3794
- });
3795
- }
3796
- else if (shuttingDown) {
3797
- await failTaskWithRetry(client, task.id, {
3885
+ availableSkillPaths = normalizeTelemetrySkillPaths(stagedPluginPaths);
3886
+ await client.workerCreateAgentTaskEvent(task.id, {
3798
3887
  workerKey,
3799
3888
  leaseToken,
3800
- error: 'worker_shutdown',
3801
- result: agentRunResult,
3889
+ kind: 'progress',
3890
+ phase: 'setup',
3891
+ message: devPluginWorkingTree
3892
+ ? 'Staged PlayDrop plugin references from explicit dev working tree'
3893
+ : 'Staged PlayDrop plugin references from server task bundle',
3894
+ pct: 3,
3895
+ payload: {
3896
+ pluginSource: devPluginWorkingTree ? 'DEV_WORKING_TREE' : 'SERVER_TASK_BUNDLE',
3897
+ bundleSha: assignment.pluginBundle.sha256,
3898
+ },
3802
3899
  });
3803
- await reportTelemetry('FAILED');
3804
- }
3805
- else if (completedAgentResult.timedOut || completedAgentResult.exitCode !== 0 || completedAgentResult.signal) {
3806
- const refreshed = await fetchTaskDetail(client, target, task.id);
3807
- if (refreshed.task.status !== 'RUNNING') {
3808
- retainWorkspace = refreshed.task.status === 'FAILED';
3809
- console.error(`Agent task ${task.id} was finalized by the agent as ${refreshed.task.status}.`);
3810
- await reportTelemetry(refreshed.task.status);
3811
- }
3812
- else if (hasAgentTaskUploadedArtifact(refreshed.task)) {
3813
- retainWorkspace = true;
3814
- console.error(`Agent task ${task.id} already uploaded an artifact but exited before task completion. Leaving it for manual completion repair.`);
3900
+ if (assignment.workspace.files.length > 0) {
3815
3901
  await client.workerCreateAgentTaskEvent(task.id, {
3816
3902
  workerKey,
3817
3903
  leaseToken,
3818
3904
  kind: 'progress',
3819
- phase: 'upload',
3820
- message: 'Agent exited after upload before task done; manual completion repair required.',
3905
+ phase: 'setup',
3906
+ message: `Staging ${assignment.workspace.files.length} server-provided workspace file${assignment.workspace.files.length === 1 ? '' : 's'}`,
3907
+ pct: 3,
3821
3908
  });
3822
- await reportTelemetry('RUNNING');
3909
+ await stageAssignmentWorkspaceFiles(workspaceDir, assignment.workspace.files);
3823
3910
  }
3824
- else {
3825
- retainWorkspace = true;
3826
- const failureCode = buildAgentFailureCode(assignment.agent, completedAgentResult);
3911
+ heartbeatTimer = setInterval(() => {
3912
+ client.workerHeartbeatAgentTask(task.id, { workerKey, leaseToken })
3913
+ .then((heartbeat) => {
3914
+ heartbeatTransientFailures = 0;
3915
+ if (heartbeat.action === 'abort') {
3916
+ fenced = true;
3917
+ activeTerminators.get(task.id)?.();
3918
+ }
3919
+ })
3920
+ .catch((error) => {
3921
+ const classification = classifyWorkerHeartbeatError(error);
3922
+ if (classification === 'session_expired') {
3923
+ handleSessionExpiry();
3924
+ return;
3925
+ }
3926
+ if (classification === 'lease_invalid') {
3927
+ void (async () => {
3928
+ const refreshed = await fetchTaskDetail(client, target, task.id).catch(() => null);
3929
+ if (refreshed?.task.status === 'DONE') {
3930
+ fenced = true;
3931
+ if (!heartbeatObservedFinalizedTask) {
3932
+ heartbeatObservedFinalizedTask = true;
3933
+ console.error(`Agent task ${task.id} lease ended after task completed as DONE; waiting for agent exit.`);
3934
+ }
3935
+ return;
3936
+ }
3937
+ if (refreshed && refreshed.task.status !== 'RUNNING' && refreshed.task.status !== 'QUEUED') {
3938
+ console.error(`Agent task ${task.id} lease ended after task finalized as ${refreshed.task.status}; terminating agent child.`);
3939
+ }
3940
+ fenced = true;
3941
+ activeTerminators.get(task.id)?.();
3942
+ })().catch((heartbeatError) => {
3943
+ fenced = true;
3944
+ activeTerminators.get(task.id)?.();
3945
+ console.error(`Agent task lease check failed: ${heartbeatError instanceof Error ? heartbeatError.message : String(heartbeatError)}`);
3946
+ });
3947
+ return;
3948
+ }
3949
+ if (classification === 'transient_network') {
3950
+ heartbeatTransientFailures += 1;
3951
+ console.error(`Agent task heartbeat transient failure ${heartbeatTransientFailures}/${TASK_HEARTBEAT_TRANSIENT_FAILURE_LIMIT}: ${error instanceof Error ? error.message : String(error)}`);
3952
+ if (heartbeatTransientFailures >= TASK_HEARTBEAT_TRANSIENT_FAILURE_LIMIT) {
3953
+ fenced = true;
3954
+ activeTerminators.get(task.id)?.();
3955
+ console.error(`Agent task ${task.id} heartbeat could not reach the API; terminating agent child.`);
3956
+ }
3957
+ return;
3958
+ }
3959
+ console.error(`Agent task heartbeat failed: ${error instanceof Error ? error.message : String(error)}`);
3960
+ });
3961
+ }, task.kind === 'GAME_REVIEW' || task.kind === 'GAME_EVAL'
3962
+ ? INSTRUMENT_HEARTBEAT_INTERVAL_MS
3963
+ : HEARTBEAT_INTERVAL_MS);
3964
+ eventDrainTimer = setInterval(() => {
3965
+ queueEventDrain().catch((error) => {
3966
+ handleEventDrainFailure(error);
3967
+ });
3968
+ }, 1000);
3969
+ const prompt = assignment.prompt;
3970
+ if (task.kind === 'GAME_UPDATE') {
3971
+ const baseSourcePayload = assignment.inputs.baseSource;
3972
+ const baseSource = resolveWorkerBaseSource(baseSourcePayload);
3827
3973
  await client.workerCreateAgentTaskEvent(task.id, {
3828
3974
  workerKey,
3829
3975
  leaseToken,
3830
3976
  kind: 'progress',
3831
- phase: 'failed',
3832
- message: describeAgentFailureForEvent(failureCode),
3833
- pct: null,
3834
- payload: { error: failureCode },
3977
+ phase: 'setup',
3978
+ message: `Staging base source ${baseSource.appName} version ${baseSource.version} into the workspace`,
3979
+ pct: 4,
3980
+ });
3981
+ await stageWorkerBaseAppSource({
3982
+ client,
3983
+ workspaceDir,
3984
+ creatorUsername: baseSourcePayload.creatorUsername,
3985
+ baseApp: baseSource,
3986
+ });
3987
+ }
3988
+ else if (task.kind === 'NEW_GAME' || task.kind === 'REMIX_GAME') {
3989
+ if (assignment.inputs.baseSource !== null) {
3990
+ throw new Error('agent_task_assignment_unexpected_base_source');
3991
+ }
3992
+ if (task.kind === 'REMIX_GAME' && !assignment.inputs.remixSource) {
3993
+ throw new Error('agent_task_assignment_remix_source_missing');
3994
+ }
3995
+ }
3996
+ else if (task.kind === 'GAME_REVIEW') {
3997
+ if (assignment.inputs.baseSource !== null) {
3998
+ throw new Error('agent_task_assignment_unexpected_base_source');
3999
+ }
4000
+ if (!assignment.inputs.reviewTarget) {
4001
+ throw new Error('agent_task_assignment_review_target_missing');
4002
+ }
4003
+ }
4004
+ else if (task.kind === 'GAME_EVAL') {
4005
+ if (assignment.inputs.baseSource !== null) {
4006
+ throw new Error('agent_task_assignment_unexpected_base_source');
4007
+ }
4008
+ if (!assignment.inputs.evalTarget) {
4009
+ throw new Error('agent_task_assignment_eval_target_missing');
4010
+ }
4011
+ }
4012
+ else {
4013
+ throw new Error(`unsupported_agent_task_kind:${task.kind}`);
4014
+ }
4015
+ if (shuttingDown) {
4016
+ throw new Error('worker_shutdown');
4017
+ }
4018
+ agentStartedAt = new Date();
4019
+ if (assignment.agent === 'CODEX') {
4020
+ agentResult = await runCodex({
4021
+ workspaceDir,
4022
+ binDir,
4023
+ eventDir,
4024
+ prompt,
4025
+ envName: env,
4026
+ workerUsername: username,
4027
+ taskId: task.id,
4028
+ attempt: taskContext.attempt,
4029
+ devPort,
4030
+ codexModel: codexModel ?? resolveCodexModel(assignment.model),
4031
+ onTranscriptChunks: async (chunks) => {
4032
+ await appendTranscriptChunks(chunks);
4033
+ },
4034
+ onChild: (controls) => {
4035
+ activeTerminators.set(task.id, controls.terminate);
4036
+ },
4037
+ });
4038
+ }
4039
+ else if (assignment.agent === 'CLAUDE_CODE') {
4040
+ agentResult = await runClaude({
4041
+ workspaceDir,
4042
+ binDir,
4043
+ eventDir,
4044
+ prompt,
4045
+ enableChrome: task.kind === 'GAME_REVIEW' || task.kind === 'GAME_EVAL',
4046
+ envName: env,
4047
+ workerUsername: username,
4048
+ taskId: task.id,
4049
+ attempt: taskContext.attempt,
4050
+ devPort,
4051
+ claudeModel: claudeModel ?? resolveClaudeModel(assignment.model),
4052
+ taskPluginRoot: assignmentPluginRoot,
4053
+ onTranscriptChunks: async (chunks) => {
4054
+ await appendTranscriptChunks(chunks);
4055
+ },
4056
+ onChild: (controls) => {
4057
+ activeTerminators.set(task.id, controls.terminate);
4058
+ },
4059
+ });
4060
+ }
4061
+ else if (assignment.agent === 'CURSOR_COMPOSER') {
4062
+ const assignmentAgent = assignment.agent;
4063
+ const assignmentModel = assignment.model;
4064
+ const plannedAttemptIndex = assignment.attemptPlan?.options.findIndex((option) => option.agent === assignmentAgent && option.model === assignmentModel);
4065
+ const attemptIndex = typeof plannedAttemptIndex === 'number' && plannedAttemptIndex >= 0
4066
+ ? plannedAttemptIndex
4067
+ : 0;
4068
+ await client.workerRecordAgentTaskAttemptUnavailable(task.id, {
4069
+ workerKey,
4070
+ leaseToken,
4071
+ attemptIndex,
4072
+ reason: 'unavailable_runtime',
4073
+ message: 'Cursor Composer noninteractive worker execution is not configured on this worker.',
3835
4074
  });
4075
+ fenced = true;
4076
+ throw new Error('worker_cursor_composer_runner_not_configured');
4077
+ }
4078
+ else {
4079
+ throw new Error(`unsupported_worker_agent:${assignment.agent}`);
4080
+ }
4081
+ agentCompletedAt = new Date();
4082
+ await queueEventDrain().catch((error) => {
4083
+ handleEventDrainFailure(error);
4084
+ });
4085
+ const completedAgentResult = agentResult;
4086
+ const agentRunResult = buildAgentRunResult(completedAgentResult);
4087
+ if (fenced) {
4088
+ const refreshed = await fetchTaskDetail(client, target, task.id).catch(() => null);
4089
+ if (refreshed && refreshed.task.status !== 'RUNNING' && refreshed.task.status !== 'QUEUED') {
4090
+ retainWorkspace = refreshed.task.status === 'FAILED';
4091
+ console.error(`Agent task ${task.id} was finalized by the server as ${refreshed.task.status}.`);
4092
+ await reportTelemetry(refreshed.task.status);
4093
+ }
4094
+ else {
4095
+ console.error(`Agent task ${task.id} was aborted by the server; cleaning up without reporting failure.`);
4096
+ }
4097
+ }
4098
+ else if (sessionExpired) {
4099
+ // Best effort: the session is already invalid, so this fail call
4100
+ // likely 401s too; failTaskWithRetry logs loudly after the retry.
3836
4101
  await failTaskWithRetry(client, task.id, {
3837
4102
  workerKey,
3838
4103
  leaseToken,
3839
- error: failureCode,
4104
+ error: 'worker_session_expired',
3840
4105
  result: agentRunResult,
3841
4106
  });
3842
- await reportTelemetry('FAILED');
3843
- }
3844
- }
3845
- else {
3846
- const refreshed = await fetchTaskDetail(client, target, task.id);
3847
- if (refreshed.task.status !== 'RUNNING') {
3848
- retainWorkspace = refreshed.task.status === 'FAILED';
3849
- console.error(`Agent task ${task.id} was finalized by the agent as ${refreshed.task.status}.`);
3850
- await reportTelemetry(refreshed.task.status);
3851
4107
  }
3852
- else if (hasAgentTaskUploadedArtifact(refreshed.task)) {
3853
- retainWorkspace = true;
3854
- console.error(`Agent task ${task.id} already uploaded an artifact but exited before task completion. Leaving it for manual completion repair.`);
3855
- await client.workerCreateAgentTaskEvent(task.id, {
4108
+ else if (shuttingDown) {
4109
+ await failTaskWithRetry(client, task.id, {
3856
4110
  workerKey,
3857
4111
  leaseToken,
3858
- kind: 'progress',
3859
- phase: 'upload',
3860
- message: 'Agent exited after upload before task done; manual completion repair required.',
4112
+ error: 'worker_shutdown',
4113
+ result: agentRunResult,
3861
4114
  });
3862
- await reportTelemetry('RUNNING');
4115
+ await reportTelemetry('FAILED');
3863
4116
  }
3864
- else {
3865
- const tokenCap = (0, runtime_1.readPositiveEnvInt)('PLAYDROP_WORKER_TOKEN_CAP', runtime_1.DEFAULT_WORKER_TOKEN_CAP);
3866
- let tokenUsageKnown = true;
3867
- try {
3868
- tokenUsageKnown = (0, runtime_1.assertWorkerTokenUsageWithinCap)({
3869
- tokensUsed: agentResult.tokensUsed,
3870
- tokenCap,
4117
+ else if (completedAgentResult.timedOut || completedAgentResult.exitCode !== 0 || completedAgentResult.signal) {
4118
+ const refreshed = await fetchTaskDetail(client, target, task.id);
4119
+ if (refreshed.task.status !== 'RUNNING') {
4120
+ retainWorkspace = refreshed.task.status === 'FAILED';
4121
+ console.error(`Agent task ${task.id} was finalized by the agent as ${refreshed.task.status}.`);
4122
+ await reportTelemetry(refreshed.task.status);
4123
+ }
4124
+ else if (hasAgentTaskUploadedArtifact(refreshed.task)) {
4125
+ retainWorkspace = true;
4126
+ console.error(`Agent task ${task.id} already uploaded an artifact but exited before task completion. Leaving it for manual completion repair.`);
4127
+ await client.workerCreateAgentTaskEvent(task.id, {
4128
+ workerKey,
4129
+ leaseToken,
4130
+ kind: 'progress',
4131
+ phase: 'upload',
4132
+ message: 'Agent exited after upload before task done; manual completion repair required.',
3871
4133
  });
4134
+ await reportTelemetry('RUNNING');
3872
4135
  }
3873
- catch (error) {
4136
+ else {
4137
+ retainWorkspace = true;
4138
+ const failureCode = buildAgentFailureCode(assignment.agent, completedAgentResult);
3874
4139
  await client.workerCreateAgentTaskEvent(task.id, {
3875
4140
  workerKey,
3876
4141
  leaseToken,
3877
4142
  kind: 'progress',
3878
- message: `Token usage ${completedAgentResult.tokensUsed} exceeded the worker token cap of ${tokenCap}.`,
4143
+ phase: 'failed',
4144
+ message: describeAgentFailureForEvent(failureCode),
4145
+ pct: null,
4146
+ payload: { error: failureCode },
4147
+ });
4148
+ await failTaskWithRetry(client, task.id, {
4149
+ workerKey,
4150
+ leaseToken,
4151
+ error: failureCode,
4152
+ result: agentRunResult,
3879
4153
  });
3880
4154
  await reportTelemetry('FAILED');
3881
- throw error;
3882
4155
  }
3883
- if (!tokenUsageKnown) {
4156
+ }
4157
+ else {
4158
+ const refreshed = await fetchTaskDetail(client, target, task.id);
4159
+ if (refreshed.task.status !== 'RUNNING') {
4160
+ retainWorkspace = refreshed.task.status === 'FAILED';
4161
+ console.error(`Agent task ${task.id} was finalized by the agent as ${refreshed.task.status}.`);
4162
+ await reportTelemetry(refreshed.task.status);
4163
+ }
4164
+ else if (hasAgentTaskUploadedArtifact(refreshed.task)) {
4165
+ retainWorkspace = true;
4166
+ console.error(`Agent task ${task.id} already uploaded an artifact but exited before task completion. Leaving it for manual completion repair.`);
3884
4167
  await client.workerCreateAgentTaskEvent(task.id, {
3885
4168
  workerKey,
3886
4169
  leaseToken,
3887
4170
  kind: 'progress',
3888
- message: 'Token usage parse failed: the agent output did not contain a recognizable total token count.',
4171
+ phase: 'upload',
4172
+ message: 'Agent exited after upload before task done; manual completion repair required.',
3889
4173
  });
4174
+ await reportTelemetry('RUNNING');
4175
+ }
4176
+ else {
4177
+ const tokenCap = (0, runtime_1.readPositiveEnvInt)('PLAYDROP_WORKER_TOKEN_CAP', runtime_1.DEFAULT_WORKER_TOKEN_CAP);
4178
+ let tokenUsageKnown = true;
4179
+ try {
4180
+ tokenUsageKnown = (0, runtime_1.assertWorkerTokenUsageWithinCap)({
4181
+ tokensUsed: agentResult.tokensUsed,
4182
+ tokenCap,
4183
+ });
4184
+ }
4185
+ catch (error) {
4186
+ await client.workerCreateAgentTaskEvent(task.id, {
4187
+ workerKey,
4188
+ leaseToken,
4189
+ kind: 'progress',
4190
+ message: `Token usage ${completedAgentResult.tokensUsed} exceeded the worker token cap of ${tokenCap}.`,
4191
+ });
4192
+ await reportTelemetry('FAILED');
4193
+ throw error;
4194
+ }
4195
+ if (!tokenUsageKnown) {
4196
+ await client.workerCreateAgentTaskEvent(task.id, {
4197
+ workerKey,
4198
+ leaseToken,
4199
+ kind: 'progress',
4200
+ message: 'Token usage parse failed: the agent output did not contain a recognizable total token count.',
4201
+ });
4202
+ }
4203
+ await client.workerCreateAgentTaskEvent(task.id, {
4204
+ workerKey,
4205
+ leaseToken,
4206
+ kind: 'progress',
4207
+ phase: 'failed',
4208
+ message: 'Agent exited before running playdrop task done',
4209
+ });
4210
+ retainWorkspace = true;
4211
+ await failTaskWithRetry(client, task.id, {
4212
+ workerKey,
4213
+ leaseToken,
4214
+ error: 'agent_exited_without_task_done',
4215
+ result: agentRunResult,
4216
+ });
4217
+ await reportTelemetry('FAILED');
3890
4218
  }
3891
- await client.workerCreateAgentTaskEvent(task.id, {
3892
- workerKey,
3893
- leaseToken,
3894
- kind: 'progress',
3895
- phase: 'failed',
3896
- message: 'Agent exited before running playdrop task done',
3897
- });
3898
- retainWorkspace = true;
3899
- await failTaskWithRetry(client, task.id, {
3900
- workerKey,
3901
- leaseToken,
3902
- error: 'agent_exited_without_task_done',
3903
- result: agentRunResult,
3904
- });
3905
- await reportTelemetry('FAILED');
3906
4219
  }
3907
4220
  }
3908
- }
3909
- catch (error) {
3910
- const message = error instanceof Error ? error.message : String(error);
3911
- if (isAgentTaskNotRunningError(error)) {
3912
- const refreshed = await fetchTaskDetail(client, target, task.id).catch(() => null);
3913
- retainWorkspace = refreshed?.task.status === 'FAILED';
3914
- console.error(`Agent task ${task.id} was already finalized${refreshed ? ` as ${refreshed.task.status}` : ''}.`);
3915
- }
3916
- else {
3917
- retainWorkspace = true;
3918
- console.error(`Agent task ${task.id} failed in the worker: ${message}`);
3919
- }
3920
- if (!fenced && !isAgentTaskNotRunningError(error)) {
3921
- try {
3922
- await failTaskWithRetry(client, task.id, {
3923
- workerKey,
3924
- leaseToken,
3925
- error: normalizeWorkerFailureErrorCode(message),
3926
- result: { workerError: message },
3927
- });
4221
+ catch (error) {
4222
+ const message = error instanceof Error ? error.message : String(error);
4223
+ if (isAgentTaskNotRunningError(error)) {
4224
+ const refreshed = await fetchTaskDetail(client, target, task.id).catch(() => null);
4225
+ retainWorkspace = refreshed?.task.status === 'FAILED';
4226
+ console.error(`Agent task ${task.id} was already finalized${refreshed ? ` as ${refreshed.task.status}` : ''}.`);
3928
4227
  }
3929
- finally {
3930
- if (!telemetryReported) {
3931
- try {
3932
- await reportTelemetry('FAILED', message);
3933
- }
3934
- catch (telemetryError) {
3935
- console.error(`Agent task telemetry failed: ${telemetryError instanceof Error ? telemetryError.message : String(telemetryError)}`);
4228
+ else {
4229
+ retainWorkspace = true;
4230
+ console.error(`Agent task ${task.id} failed in the worker: ${message}`);
4231
+ }
4232
+ if (!fenced && !isAgentTaskNotRunningError(error)) {
4233
+ try {
4234
+ await failTaskWithRetry(client, task.id, {
4235
+ workerKey,
4236
+ leaseToken,
4237
+ error: normalizeWorkerFailureErrorCode(message),
4238
+ result: { workerError: message },
4239
+ });
4240
+ }
4241
+ finally {
4242
+ if (!telemetryReported) {
4243
+ try {
4244
+ await reportTelemetry('FAILED', message);
4245
+ }
4246
+ catch (telemetryError) {
4247
+ console.error(`Agent task telemetry failed: ${telemetryError instanceof Error ? telemetryError.message : String(telemetryError)}`);
4248
+ }
3936
4249
  }
3937
4250
  }
3938
4251
  }
3939
4252
  }
3940
- }
3941
- finally {
3942
- if (heartbeatTimer) {
3943
- clearInterval(heartbeatTimer);
4253
+ finally {
4254
+ if (heartbeatTimer) {
4255
+ clearInterval(heartbeatTimer);
4256
+ }
4257
+ if (eventDrainTimer) {
4258
+ clearInterval(eventDrainTimer);
4259
+ }
4260
+ if (assignment
4261
+ && agentStartedAt
4262
+ && (assignment.kind === 'GAME_REVIEW' || assignment.kind === 'GAME_EVAL')) {
4263
+ try {
4264
+ const hygiene = await enforceInstrumentBrowserHygiene(assignment);
4265
+ console.log(`Agent task ${task.id} browser hygiene passed: ${hygiene.remainingTaskTabs} task tabs, ${hygiene.blankWindows} blank window(s).`);
4266
+ }
4267
+ catch (hygieneError) {
4268
+ const error = hygieneError instanceof Error ? hygieneError : new Error(String(hygieneError));
4269
+ console.error(`Agent task ${task.id} browser hygiene failed: ${error.message}`);
4270
+ retainWorkspace = true;
4271
+ recordFatalTaskError(error);
4272
+ }
4273
+ }
4274
+ activeTerminators.delete(task.id);
4275
+ if (workspaceDir) {
4276
+ await removeAgentRunCredentials(workspaceDir);
4277
+ }
4278
+ if (retainWorkspace && (0, runtime_1.readEnvBoolean)('PLAYDROP_WORKER_RETAIN_FAILED_WORKSPACE', true)) {
4279
+ console.error(`Retained failed task workspace for debugging: ${workspaceDir ?? '(not created)'}`);
4280
+ }
4281
+ else if (workspaceDir) {
4282
+ await (0, promises_1.rm)(workspaceDir, { recursive: true, force: true });
4283
+ }
3944
4284
  }
3945
- if (eventDrainTimer) {
3946
- clearInterval(eventDrainTimer);
4285
+ if (sessionExpired) {
4286
+ throw new Error(exports.WORKER_SESSION_EXPIRED_MESSAGE);
3947
4287
  }
3948
- if (assignment
3949
- && agentStartedAt
3950
- && (assignment.kind === 'GAME_REVIEW' || assignment.kind === 'GAME_EVAL')) {
4288
+ };
4289
+ try {
4290
+ let claimBackoffMs = CLAIM_BACKOFF_BASE_MS;
4291
+ while (!shuttingDown && !quiescingForUpdate) {
4292
+ if (fatalTaskError) {
4293
+ throwWorkerRuntimeError(fatalTaskError);
4294
+ }
4295
+ if (sessionExpired) {
4296
+ throw new Error(exports.WORKER_SESSION_EXPIRED_MESSAGE);
4297
+ }
4298
+ if (activeTaskRuns.size === 0
4299
+ && Date.now() - lastCapabilityRefreshAt >= WORKER_CAPABILITY_REFRESH_INTERVAL_MS) {
4300
+ capabilities = probeReadyWorkerCapabilities({
4301
+ npmVersion,
4302
+ playwright,
4303
+ maxParallelTasks,
4304
+ });
4305
+ lastCapabilityRefreshAt = Date.now();
4306
+ console.log('Worker refreshed installed agent CLI versions and model catalogs.');
4307
+ }
4308
+ if (activeTaskRuns.size >= maxParallelTasks) {
4309
+ await sleep(DEFAULT_POLL_INTERVAL_MS);
4310
+ continue;
4311
+ }
4312
+ const devPort = allocateWorkerDevPort({
4313
+ activePorts: activeDevPorts,
4314
+ basePort: workerDevPortBase,
4315
+ maxParallelTasks,
4316
+ });
4317
+ let claim;
3951
4318
  try {
3952
- const hygiene = await enforceInstrumentBrowserHygiene(assignment);
3953
- console.log(`Agent task ${task.id} browser hygiene passed: ${hygiene.remainingTaskTabs} task tabs, ${hygiene.blankWindows} blank window(s).`);
4319
+ claim = await client.workerClaimAgentTask(buildWorkerClaimBody({
4320
+ workerKey,
4321
+ capabilities,
4322
+ runningTaskCount: activeTaskRuns.size,
4323
+ }));
4324
+ handleWorkerAction(claim);
4325
+ claimBackoffMs = CLAIM_BACKOFF_BASE_MS;
3954
4326
  }
3955
- catch (hygieneError) {
3956
- const error = hygieneError instanceof Error ? hygieneError : new Error(String(hygieneError));
3957
- console.error(`Agent task ${task.id} browser hygiene failed: ${error.message}`);
3958
- retainWorkspace = true;
4327
+ catch (error) {
4328
+ // Auth (401) and forbidden (403) claim errors stop the worker; only
4329
+ // transient errors reach the backoff below.
4330
+ assertWorkerClaimErrorRetryable(error);
4331
+ console.error(`Agent task claim failed: ${error instanceof Error ? error.message : String(error)}`);
4332
+ await sleep(claimBackoffDelayMs(claimBackoffMs));
4333
+ claimBackoffMs = nextClaimBackoffMs(claimBackoffMs);
4334
+ continue;
4335
+ }
4336
+ const task = claim.task;
4337
+ if (!task) {
4338
+ if (shuttingDown || quiescingForUpdate) {
4339
+ break;
4340
+ }
4341
+ if (options.once) {
4342
+ return;
4343
+ }
4344
+ await sleep(DEFAULT_POLL_INTERVAL_MS);
4345
+ continue;
4346
+ }
4347
+ activeTaskIds.add(task.id);
4348
+ activeDevPorts.add(devPort);
4349
+ const taskRun = runClaimedTask(claim, devPort)
4350
+ .catch((error) => {
3959
4351
  recordFatalTaskError(error);
4352
+ })
4353
+ .finally(() => {
4354
+ activeTaskRuns.delete(task.id);
4355
+ activeTaskIds.delete(task.id);
4356
+ activeDevPorts.delete(devPort);
4357
+ });
4358
+ activeTaskRuns.set(task.id, taskRun);
4359
+ if (options.once) {
4360
+ await taskRun;
4361
+ if (fatalTaskError) {
4362
+ throwWorkerRuntimeError(fatalTaskError);
4363
+ }
4364
+ return;
3960
4365
  }
3961
4366
  }
3962
- activeTerminators.delete(task.id);
3963
- if (retainWorkspace && (0, runtime_1.readEnvBoolean)('PLAYDROP_WORKER_RETAIN_FAILED_WORKSPACE', true)) {
3964
- console.error(`Retained failed task workspace for debugging: ${workspaceDir ?? '(not created)'}`);
4367
+ if (activeTaskRuns.size > 0) {
4368
+ await Promise.allSettled(Array.from(activeTaskRuns.values()));
3965
4369
  }
3966
- else if (workspaceDir) {
3967
- await (0, promises_1.rm)(workspaceDir, { recursive: true, force: true });
3968
- }
3969
- }
3970
- if (sessionExpired) {
3971
- throw new Error(exports.WORKER_SESSION_EXPIRED_MESSAGE);
3972
- }
3973
- };
3974
- try {
3975
- let claimBackoffMs = CLAIM_BACKOFF_BASE_MS;
3976
- while (!shuttingDown && !quiescingForUpdate) {
3977
4370
  if (fatalTaskError) {
3978
4371
  throwWorkerRuntimeError(fatalTaskError);
3979
4372
  }
3980
4373
  if (sessionExpired) {
3981
4374
  throw new Error(exports.WORKER_SESSION_EXPIRED_MESSAGE);
3982
4375
  }
3983
- if (activeTaskRuns.size === 0
3984
- && Date.now() - lastCapabilityRefreshAt >= WORKER_CAPABILITY_REFRESH_INTERVAL_MS) {
3985
- capabilities = probeReadyWorkerCapabilities({
3986
- npmVersion,
3987
- playwright,
3988
- maxParallelTasks,
3989
- pluginVersion: playdropPluginVersion,
3990
- });
3991
- lastCapabilityRefreshAt = Date.now();
3992
- console.log('Worker refreshed installed agent CLI versions and model catalogs.');
3993
- }
3994
- if (activeTaskRuns.size >= maxParallelTasks) {
3995
- await sleep(DEFAULT_POLL_INTERVAL_MS);
3996
- continue;
3997
- }
3998
- const devPort = allocateWorkerDevPort({
3999
- activePorts: activeDevPorts,
4000
- basePort: workerDevPortBase,
4001
- maxParallelTasks,
4002
- });
4003
- let claim;
4004
- try {
4005
- claim = await client.workerClaimAgentTask(buildWorkerClaimBody({
4006
- workerKey,
4007
- capabilities,
4008
- runningTaskCount: activeTaskRuns.size,
4009
- }));
4010
- handleWorkerAction(claim);
4011
- claimBackoffMs = CLAIM_BACKOFF_BASE_MS;
4012
- }
4013
- catch (error) {
4014
- // Auth (401) and forbidden (403) claim errors stop the worker; only
4015
- // transient errors reach the backoff below.
4016
- assertWorkerClaimErrorRetryable(error);
4017
- console.error(`Agent task claim failed: ${error instanceof Error ? error.message : String(error)}`);
4018
- await sleep(claimBackoffDelayMs(claimBackoffMs));
4019
- claimBackoffMs = nextClaimBackoffMs(claimBackoffMs);
4020
- continue;
4021
- }
4022
- const task = claim.task;
4023
- if (!task) {
4024
- if (shuttingDown || quiescingForUpdate) {
4025
- break;
4026
- }
4027
- if (options.once) {
4028
- return;
4029
- }
4030
- await sleep(DEFAULT_POLL_INTERVAL_MS);
4031
- continue;
4376
+ if (quiescingForUpdate) {
4377
+ console.log('Worker quiesced for managed update. Exiting for supervisor restart.');
4378
+ return;
4032
4379
  }
4033
- activeTaskIds.add(task.id);
4034
- activeDevPorts.add(devPort);
4035
- const taskRun = runClaimedTask(claim, devPort)
4036
- .catch((error) => {
4037
- recordFatalTaskError(error);
4038
- })
4039
- .finally(() => {
4040
- activeTaskRuns.delete(task.id);
4041
- activeTaskIds.delete(task.id);
4042
- activeDevPorts.delete(devPort);
4043
- });
4044
- activeTaskRuns.set(task.id, taskRun);
4045
- if (options.once) {
4046
- await taskRun;
4047
- if (fatalTaskError) {
4048
- throwWorkerRuntimeError(fatalTaskError);
4049
- }
4380
+ if (shuttingDown) {
4381
+ await sendWorkerHealthAlertSafely({
4382
+ state: 'stopped',
4383
+ env,
4384
+ workerName,
4385
+ taskId: firstActiveTaskId(),
4386
+ detail: shutdownSignal,
4387
+ });
4388
+ node_process_1.default.exitCode = shutdownSignal === 'SIGINT' ? 130 : 143;
4050
4389
  return;
4051
4390
  }
4052
4391
  }
4053
- if (activeTaskRuns.size > 0) {
4054
- await Promise.allSettled(Array.from(activeTaskRuns.values()));
4055
- }
4056
- if (fatalTaskError) {
4057
- throwWorkerRuntimeError(fatalTaskError);
4058
- }
4059
- if (sessionExpired) {
4060
- throw new Error(exports.WORKER_SESSION_EXPIRED_MESSAGE);
4061
- }
4062
- if (quiescingForUpdate) {
4063
- console.log('Worker quiesced for managed update. Exiting for supervisor restart.');
4064
- return;
4065
- }
4066
- if (shuttingDown) {
4392
+ catch (error) {
4393
+ crashed = true;
4067
4394
  await sendWorkerHealthAlertSafely({
4068
- state: 'stopped',
4395
+ state: 'crashed',
4069
4396
  env,
4070
4397
  workerName,
4071
4398
  taskId: firstActiveTaskId(),
4072
- detail: shutdownSignal,
4399
+ detail: error instanceof Error ? error.message : String(error),
4073
4400
  });
4074
- node_process_1.default.exit(shutdownSignal === 'SIGINT' ? 130 : 143);
4401
+ throw error;
4402
+ }
4403
+ finally {
4404
+ if (presenceTimer) {
4405
+ clearInterval(presenceTimer);
4406
+ }
4407
+ node_process_1.default.off('SIGINT', signalHandler);
4408
+ node_process_1.default.off('SIGTERM', signalHandler);
4409
+ if (options.superviseStdin) {
4410
+ node_process_1.default.stdin.off('end', stdinShutdownHandler);
4411
+ node_process_1.default.stdin.off('close', stdinShutdownHandler);
4412
+ }
4413
+ if (!crashed) {
4414
+ await sendWorkerHealthAlertSafely({
4415
+ state: 'stopped',
4416
+ env,
4417
+ workerName,
4418
+ taskId: firstActiveTaskId(),
4419
+ });
4420
+ }
4421
+ clearWorkerRuntimeState(target);
4075
4422
  }
4076
- }
4077
- catch (error) {
4078
- crashed = true;
4079
- await sendWorkerHealthAlertSafely({
4080
- state: 'crashed',
4081
- env,
4082
- workerName,
4083
- taskId: firstActiveTaskId(),
4084
- detail: error instanceof Error ? error.message : String(error),
4085
- });
4086
- throw error;
4087
4423
  }
4088
4424
  finally {
4089
- if (presenceTimer) {
4090
- clearInterval(presenceTimer);
4091
- }
4092
- node_process_1.default.off('SIGINT', signalHandler);
4093
- node_process_1.default.off('SIGTERM', signalHandler);
4094
- if (options.superviseStdin) {
4095
- node_process_1.default.stdin.off('end', stdinShutdownHandler);
4096
- node_process_1.default.stdin.off('close', stdinShutdownHandler);
4097
- }
4098
- if (!crashed) {
4099
- await sendWorkerHealthAlertSafely({
4100
- state: 'stopped',
4101
- env,
4102
- workerName,
4103
- taskId: firstActiveTaskId(),
4104
- });
4105
- }
4106
- clearWorkerRuntimeState(target);
4425
+ releaseWorkerSupervisorLock(supervisorLock);
4107
4426
  }
4108
4427
  }, { env: options.env });
4109
4428
  }