@playdrop/playdrop-cli 0.12.7 → 0.12.8

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