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