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