aglio-worker 0.1.0 → 0.6.0
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/agent.js +152 -65
- package/package.json +1 -1
package/agent.js
CHANGED
|
@@ -7345,8 +7345,10 @@ var NOTIFICATION_TYPES = [
|
|
|
7345
7345
|
"run_finished",
|
|
7346
7346
|
"run_failed",
|
|
7347
7347
|
"review_feedback",
|
|
7348
|
+
"pr_ready",
|
|
7348
7349
|
"pr_merged",
|
|
7349
|
-
"mention"
|
|
7350
|
+
"mention",
|
|
7351
|
+
"comment"
|
|
7350
7352
|
];
|
|
7351
7353
|
var CREDENTIAL_SCOPES = ["workspace", "user"];
|
|
7352
7354
|
var CREDENTIAL_AUTH_TYPES = ["api_key", "oauth_token", "session_bundle"];
|
|
@@ -27267,45 +27269,58 @@ var repoConnectionReportSchema = external_exports.object({
|
|
|
27267
27269
|
repos: external_exports.array(repoConnectionCheckSchema)
|
|
27268
27270
|
});
|
|
27269
27271
|
|
|
27272
|
+
// ../../packages/shared/src/notifications.ts
|
|
27273
|
+
var DEFAULT_NOTIFICATION_PREFERENCES = {
|
|
27274
|
+
emailDigest: true,
|
|
27275
|
+
emailQuestions: true,
|
|
27276
|
+
emailFailures: true,
|
|
27277
|
+
emailPrReady: true
|
|
27278
|
+
};
|
|
27279
|
+
var notificationPreferencesSchema = external_exports.object({
|
|
27280
|
+
emailDigest: external_exports.boolean().catch(DEFAULT_NOTIFICATION_PREFERENCES.emailDigest),
|
|
27281
|
+
emailQuestions: external_exports.boolean().catch(DEFAULT_NOTIFICATION_PREFERENCES.emailQuestions),
|
|
27282
|
+
emailFailures: external_exports.boolean().catch(DEFAULT_NOTIFICATION_PREFERENCES.emailFailures),
|
|
27283
|
+
emailPrReady: external_exports.boolean().catch(DEFAULT_NOTIFICATION_PREFERENCES.emailPrReady)
|
|
27284
|
+
});
|
|
27285
|
+
function isSlackWebhookUrl(value) {
|
|
27286
|
+
if (typeof value !== "string" || !value.trim()) return false;
|
|
27287
|
+
try {
|
|
27288
|
+
const url2 = new URL(value.trim());
|
|
27289
|
+
return url2.protocol === "https:";
|
|
27290
|
+
} catch {
|
|
27291
|
+
return false;
|
|
27292
|
+
}
|
|
27293
|
+
}
|
|
27294
|
+
|
|
27270
27295
|
// ../../packages/shared/src/routing.ts
|
|
27271
|
-
var ROUTING_LABEL_KEYS = ["os", "mode", "tools", "size"];
|
|
27272
27296
|
var ROUTING_OS_VALUES = ["linux", "macos"];
|
|
27273
27297
|
var ROUTING_MODE_VALUES = ["docker", "host"];
|
|
27274
|
-
var ROUTING_TOOL_VALUES = ["ios", "xcode", "android", "
|
|
27275
|
-
var ROUTING_SIZE_VALUES = ["small", "large"];
|
|
27276
|
-
var
|
|
27298
|
+
var ROUTING_TOOL_VALUES = ["ios", "xcode", "android", "gpu"];
|
|
27299
|
+
var ROUTING_SIZE_VALUES = ["small", "medium", "large"];
|
|
27300
|
+
var ROUTING_GPU_VALUES = ["t4", "a100", "h100"];
|
|
27301
|
+
var ROUTING_ARCH_VALUES = ["x64", "arm64"];
|
|
27302
|
+
var ROUTING_REGION_VALUES = ["us-east", "us-west", "eu-west", "ap-southeast"];
|
|
27303
|
+
var ROUTING_LABEL_SUGGESTIONS = {
|
|
27277
27304
|
os: ROUTING_OS_VALUES,
|
|
27278
27305
|
mode: ROUTING_MODE_VALUES,
|
|
27279
27306
|
tools: ROUTING_TOOL_VALUES,
|
|
27280
|
-
size: ROUTING_SIZE_VALUES
|
|
27281
|
-
|
|
27282
|
-
|
|
27283
|
-
|
|
27284
|
-
|
|
27285
|
-
|
|
27286
|
-
|
|
27287
|
-
|
|
27288
|
-
|
|
27289
|
-
|
|
27290
|
-
throw new Error(
|
|
27291
|
-
`Unknown routing label "${value}". Use os, mode, tools or size.`
|
|
27292
|
-
);
|
|
27293
|
-
}
|
|
27294
|
-
const allowed = ROUTING_LABEL_VALUES[key];
|
|
27295
|
-
if (!allowed.includes(match[2])) {
|
|
27296
|
-
throw new Error(
|
|
27297
|
-
`Unknown routing label "${value}". Use ${allowed.map((entry) => `${key}:${entry}`).join(", ")}.`
|
|
27298
|
-
);
|
|
27299
|
-
}
|
|
27300
|
-
return { key, value: match[2] };
|
|
27301
|
-
}
|
|
27307
|
+
size: ROUTING_SIZE_VALUES,
|
|
27308
|
+
gpu: ROUTING_GPU_VALUES,
|
|
27309
|
+
arch: ROUTING_ARCH_VALUES,
|
|
27310
|
+
region: ROUTING_REGION_VALUES
|
|
27311
|
+
};
|
|
27312
|
+
var ROUTING_LABEL_SUGGESTION_LIST = Object.entries(
|
|
27313
|
+
ROUTING_LABEL_SUGGESTIONS
|
|
27314
|
+
).flatMap(([key, values]) => values.map((value) => `${key}:${value}`));
|
|
27315
|
+
var ROUTING_LABEL_MAX_LENGTH = 64;
|
|
27316
|
+
var LABEL_PATTERN = /^([a-z][a-z0-9_-]*):([a-z0-9][a-z0-9._-]*)$/;
|
|
27302
27317
|
function parseRoutingLabels(input2) {
|
|
27303
27318
|
const values = Array.isArray(input2) ? input2 : typeof input2 === "string" ? input2.split(/[\s,]+/) : [];
|
|
27304
27319
|
const seen = /* @__PURE__ */ new Set();
|
|
27305
27320
|
const labels = [];
|
|
27306
27321
|
for (const value of values) {
|
|
27307
27322
|
if (typeof value !== "string") continue;
|
|
27308
|
-
const label = value.trim()
|
|
27323
|
+
const label = value.trim();
|
|
27309
27324
|
if (!label || seen.has(label)) continue;
|
|
27310
27325
|
seen.add(label);
|
|
27311
27326
|
labels.push(label);
|
|
@@ -27314,24 +27329,16 @@ function parseRoutingLabels(input2) {
|
|
|
27314
27329
|
}
|
|
27315
27330
|
function validateRoutingLabels(input2) {
|
|
27316
27331
|
const labels = parseRoutingLabels(input2);
|
|
27317
|
-
const seenValues = /* @__PURE__ */ new Map();
|
|
27318
|
-
const tools = /* @__PURE__ */ new Set();
|
|
27319
27332
|
for (const label of labels) {
|
|
27320
|
-
|
|
27321
|
-
|
|
27322
|
-
|
|
27323
|
-
|
|
27324
|
-
throw new Error(`Conflicting routing labels "${key}:${previous}" and "${label}".`);
|
|
27325
|
-
}
|
|
27333
|
+
if (label.length > ROUTING_LABEL_MAX_LENGTH) {
|
|
27334
|
+
throw new Error(
|
|
27335
|
+
`Invalid routing label "${label}". Labels are at most ${ROUTING_LABEL_MAX_LENGTH} characters.`
|
|
27336
|
+
);
|
|
27326
27337
|
}
|
|
27327
|
-
|
|
27328
|
-
|
|
27329
|
-
|
|
27330
|
-
|
|
27331
|
-
const mode = seenValues.get("mode");
|
|
27332
|
-
if (tools.has("ios")) {
|
|
27333
|
-
if (os && os !== "macos" || mode && mode !== "host") {
|
|
27334
|
-
throw new Error("tools:ios requires os:macos and mode:host.");
|
|
27338
|
+
if (!LABEL_PATTERN.test(label)) {
|
|
27339
|
+
throw new Error(
|
|
27340
|
+
`Invalid routing label "${label}". Use lowercase key:value, for example size:medium.`
|
|
27341
|
+
);
|
|
27335
27342
|
}
|
|
27336
27343
|
}
|
|
27337
27344
|
return labels;
|
|
@@ -27395,6 +27402,8 @@ var projectSettingsSchema = external_exports.object({
|
|
|
27395
27402
|
runRetryBackoffSeconds: external_exports.number().int().min(10).max(600).default(60),
|
|
27396
27403
|
runRetryMaxBackoffSeconds: external_exports.number().int().min(60).max(3600).default(600),
|
|
27397
27404
|
notificationSound: external_exports.boolean().default(true),
|
|
27405
|
+
/** Slack incoming webhook for question, failure and PR-ready alerts. Null disables Slack. */
|
|
27406
|
+
slackWebhookUrl: external_exports.string().trim().max(500).refine(isSlackWebhookUrl, "Slack webhook must be an https URL").nullable().default(null),
|
|
27398
27407
|
/** Rendered column strip: built-in columns and custom list ids. */
|
|
27399
27408
|
boardColumnOrder: external_exports.array(external_exports.string()).optional(),
|
|
27400
27409
|
/** Last repository connectivity check from Project settings or the worker gate. */
|
|
@@ -27594,11 +27603,12 @@ function isHostLocalSigningArtifact(name) {
|
|
|
27594
27603
|
var WORKER_HEARTBEAT_SECONDS = 15;
|
|
27595
27604
|
var WORKER_OFFLINE_AFTER_MISSES = 3;
|
|
27596
27605
|
var WORKER_OFFLINE_AFTER_SECONDS = WORKER_HEARTBEAT_SECONDS * WORKER_OFFLINE_AFTER_MISSES;
|
|
27606
|
+
var WORKER_CLAIM_LONG_POLL_SECONDS = 25;
|
|
27597
27607
|
var WORKER_MIN_VERSION = "0.1.0";
|
|
27598
27608
|
var WORKER_VERSION_HEADER = "x-aglio-worker-version";
|
|
27599
27609
|
var WORKER_MODES = ["docker", "host"];
|
|
27600
27610
|
var WORKER_OSES = ["linux", "macos"];
|
|
27601
|
-
var WORKER_LABEL_MAX_LENGTH =
|
|
27611
|
+
var WORKER_LABEL_MAX_LENGTH = ROUTING_LABEL_MAX_LENGTH;
|
|
27602
27612
|
var WORKER_IOS_LABEL = "tools:ios";
|
|
27603
27613
|
var WORKER_ENROLLMENT_TOKEN_PREFIX = "aglio_enr_";
|
|
27604
27614
|
var WORKER_TOKEN_PREFIX = "aglio_wkr_";
|
|
@@ -30748,15 +30758,38 @@ function parseClaimedJob(value) {
|
|
|
30748
30758
|
if (!lease || typeof lease.id !== "string" || typeof lease.token !== "string") return null;
|
|
30749
30759
|
return value;
|
|
30750
30760
|
}
|
|
30761
|
+
function claimHoldSeconds(config2) {
|
|
30762
|
+
const requested = config2.claimTimeoutSeconds;
|
|
30763
|
+
if (typeof requested !== "number" || !Number.isFinite(requested)) {
|
|
30764
|
+
return WORKER_CLAIM_LONG_POLL_SECONDS;
|
|
30765
|
+
}
|
|
30766
|
+
return Math.min(Math.max(Math.round(requested), 5), 120);
|
|
30767
|
+
}
|
|
30768
|
+
var CLAIM_REQUEST_GRACE_MS = 2e4;
|
|
30751
30769
|
async function claim2(config2, token, fetchImpl, signal) {
|
|
30752
|
-
const
|
|
30753
|
-
|
|
30754
|
-
|
|
30755
|
-
|
|
30756
|
-
|
|
30757
|
-
|
|
30758
|
-
|
|
30770
|
+
const holdSeconds = claimHoldSeconds(config2);
|
|
30771
|
+
const controller = new AbortController();
|
|
30772
|
+
const onAbort = () => controller.abort(signal?.reason);
|
|
30773
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
30774
|
+
const timer = setTimeout(
|
|
30775
|
+
() => controller.abort(new Error("Claim request did not answer in time.")),
|
|
30776
|
+
holdSeconds * 1e3 + CLAIM_REQUEST_GRACE_MS
|
|
30759
30777
|
);
|
|
30778
|
+
timer.unref?.();
|
|
30779
|
+
let response;
|
|
30780
|
+
try {
|
|
30781
|
+
response = await request(
|
|
30782
|
+
config2,
|
|
30783
|
+
fetchImpl,
|
|
30784
|
+
"/api/workers/claim",
|
|
30785
|
+
token,
|
|
30786
|
+
{ version: config2.version, capacity: config2.capacity, claimTimeoutSeconds: holdSeconds },
|
|
30787
|
+
controller.signal
|
|
30788
|
+
);
|
|
30789
|
+
} finally {
|
|
30790
|
+
clearTimeout(timer);
|
|
30791
|
+
signal?.removeEventListener("abort", onAbort);
|
|
30792
|
+
}
|
|
30760
30793
|
if (response.status === 204) return { status: 204, job: null, reason: null, message: null };
|
|
30761
30794
|
if (!response.ok) throw await errorFrom(response);
|
|
30762
30795
|
const body = await responseBody(response);
|
|
@@ -30767,6 +30800,15 @@ async function claim2(config2, token, fetchImpl, signal) {
|
|
|
30767
30800
|
message: typeof body.message === "string" ? body.message : null
|
|
30768
30801
|
};
|
|
30769
30802
|
}
|
|
30803
|
+
function isTransientControlPlaneError(error62) {
|
|
30804
|
+
if (error62 instanceof WorkerHttpError) return error62.status >= 500 || error62.status === 429;
|
|
30805
|
+
return true;
|
|
30806
|
+
}
|
|
30807
|
+
var CLAIM_BACKOFF_MS = [5e3, 1e4, 2e4, 3e4];
|
|
30808
|
+
function claimBackoffMs(attempt) {
|
|
30809
|
+
const index = Math.min(Math.max(Math.floor(attempt), 1), CLAIM_BACKOFF_MS.length) - 1;
|
|
30810
|
+
return CLAIM_BACKOFF_MS[index];
|
|
30811
|
+
}
|
|
30770
30812
|
async function jobTokenRequest(config2, fetchImpl, path, token, init) {
|
|
30771
30813
|
return fetchImpl(joinUrl(config2.apiUrl, path), {
|
|
30772
30814
|
method: init.method,
|
|
@@ -30805,18 +30847,23 @@ function workerCardApi(config2, fetchImpl, signal) {
|
|
|
30805
30847
|
return new Uint8Array(await response.arrayBuffer());
|
|
30806
30848
|
},
|
|
30807
30849
|
async updateRunStatus(input2) {
|
|
30808
|
-
|
|
30809
|
-
|
|
30810
|
-
|
|
30811
|
-
|
|
30812
|
-
|
|
30813
|
-
|
|
30814
|
-
|
|
30815
|
-
|
|
30816
|
-
|
|
30817
|
-
|
|
30850
|
+
await retryTransient(
|
|
30851
|
+
async () => {
|
|
30852
|
+
const response = await jobTokenRequest(
|
|
30853
|
+
config2,
|
|
30854
|
+
fetchImpl,
|
|
30855
|
+
`/api/workers/jobs/${input2.jobId}/run/status`,
|
|
30856
|
+
input2.token,
|
|
30857
|
+
{
|
|
30858
|
+
method: "POST",
|
|
30859
|
+
body: JSON.stringify({ leaseId: input2.leaseId, ...input2.patch }),
|
|
30860
|
+
signal
|
|
30861
|
+
}
|
|
30862
|
+
);
|
|
30863
|
+
if (!response.ok) throw await errorFrom(response);
|
|
30864
|
+
},
|
|
30865
|
+
{ signal }
|
|
30818
30866
|
);
|
|
30819
|
-
if (!response.ok) throw await errorFrom(response);
|
|
30820
30867
|
},
|
|
30821
30868
|
async appendRunEvents(input2) {
|
|
30822
30869
|
const response = await jobTokenRequest(
|
|
@@ -31004,6 +31051,24 @@ function jobApi(config2, fetchImpl, signal) {
|
|
|
31004
31051
|
}
|
|
31005
31052
|
};
|
|
31006
31053
|
}
|
|
31054
|
+
var TRANSIENT_RETRY_DELAYS_MS = [500, 1500];
|
|
31055
|
+
async function retryTransient(call, options = {}) {
|
|
31056
|
+
const sleep2 = options.sleep ?? ((ms) => new Promise((resolve6) => setTimeout(resolve6, ms)));
|
|
31057
|
+
let lastError;
|
|
31058
|
+
for (let attempt = 0; attempt <= TRANSIENT_RETRY_DELAYS_MS.length; attempt += 1) {
|
|
31059
|
+
try {
|
|
31060
|
+
return await call();
|
|
31061
|
+
} catch (error62) {
|
|
31062
|
+
lastError = error62;
|
|
31063
|
+
if (!isTransientControlPlaneError(error62) || options.signal?.aborted) throw error62;
|
|
31064
|
+
const delay = TRANSIENT_RETRY_DELAYS_MS[attempt];
|
|
31065
|
+
if (delay === void 0) break;
|
|
31066
|
+
options.onRetry?.(error62, attempt + 1);
|
|
31067
|
+
await sleep2(delay);
|
|
31068
|
+
}
|
|
31069
|
+
}
|
|
31070
|
+
throw lastError;
|
|
31071
|
+
}
|
|
31007
31072
|
function fatalExitCode(error62) {
|
|
31008
31073
|
if (error62.status === 401 && error62.code === "worker_revoked") return 0;
|
|
31009
31074
|
if (error62.status === 426 || error62.code === "incompatible_worker_version") return 2;
|
|
@@ -31121,10 +31186,27 @@ async function runRemoteWorker(config2, deps = {}) {
|
|
|
31121
31186
|
1,
|
|
31122
31187
|
Math.min(workerCapacity(config2.capacity, resolved.capacity), workerCapacity(resolved.capacity))
|
|
31123
31188
|
);
|
|
31189
|
+
let claimAttempt = 0;
|
|
31190
|
+
const backOffClaim = async (error62) => {
|
|
31191
|
+
claimAttempt += 1;
|
|
31192
|
+
const delay = claimBackoffMs(claimAttempt);
|
|
31193
|
+
if (claimAttempt === 1) {
|
|
31194
|
+
log("claim unavailable, backing off", {
|
|
31195
|
+
...error62 instanceof WorkerHttpError ? { status: error62.status, code: error62.code } : {},
|
|
31196
|
+
error: error62 instanceof Error ? error62.message : String(error62),
|
|
31197
|
+
retryInSeconds: Math.round(delay / 1e3)
|
|
31198
|
+
});
|
|
31199
|
+
}
|
|
31200
|
+
await sleep2(delay);
|
|
31201
|
+
};
|
|
31124
31202
|
const runSlot = async () => {
|
|
31125
31203
|
while (!controller.signal.aborted) {
|
|
31126
31204
|
try {
|
|
31127
31205
|
const result = await claim2(config2, resolved.token, fetchImpl, controller.signal);
|
|
31206
|
+
if (claimAttempt > 0) {
|
|
31207
|
+
log("claim recovered", { attempts: claimAttempt });
|
|
31208
|
+
claimAttempt = 0;
|
|
31209
|
+
}
|
|
31128
31210
|
if (result.status === 204) {
|
|
31129
31211
|
lastReason = null;
|
|
31130
31212
|
continue;
|
|
@@ -31194,11 +31276,13 @@ async function runRemoteWorker(config2, deps = {}) {
|
|
|
31194
31276
|
}
|
|
31195
31277
|
} catch (error62) {
|
|
31196
31278
|
if (controller.signal.aborted) break;
|
|
31279
|
+
if (isTransientControlPlaneError(error62)) {
|
|
31280
|
+
await backOffClaim(error62);
|
|
31281
|
+
continue;
|
|
31282
|
+
}
|
|
31197
31283
|
if (error62 instanceof WorkerHttpError) {
|
|
31198
31284
|
fail2(error62, "claim");
|
|
31199
31285
|
if (controller.signal.aborted) break;
|
|
31200
|
-
} else {
|
|
31201
|
-
log("claim failed", { error: error62 instanceof Error ? error62.message : String(error62) });
|
|
31202
31286
|
}
|
|
31203
31287
|
await sleep2(5e3);
|
|
31204
31288
|
}
|
|
@@ -43330,6 +43414,8 @@ async function main() {
|
|
|
43330
43414
|
const runnerImage = defaultRunnerImage();
|
|
43331
43415
|
const dockerSocket = process.env.AGLIO_WORKER_DOCKER_SOCKET?.trim() || "/var/run/docker.sock";
|
|
43332
43416
|
const mcpPort = Number(process.env.AGLIO_WORKER_MCP_PORT ?? 4310);
|
|
43417
|
+
const claimTimeoutRaw = Number(process.env.AGLIO_WORKER_CLAIM_TIMEOUT_SECONDS);
|
|
43418
|
+
const claimTimeoutSeconds = Number.isFinite(claimTimeoutRaw) && claimTimeoutRaw > 0 ? claimTimeoutRaw : void 0;
|
|
43333
43419
|
const agentContainerId = process.env.HOSTNAME?.trim() || hostname3();
|
|
43334
43420
|
const runnerNetwork = mode === "docker" ? resolveRunnerNetworkMode({
|
|
43335
43421
|
override: process.env.AGLIO_WORKER_RUNNER_NETWORK,
|
|
@@ -43362,6 +43448,7 @@ async function main() {
|
|
|
43362
43448
|
stateFile,
|
|
43363
43449
|
// Simulators and Xcode share one signing context on a Mac host.
|
|
43364
43450
|
capacity: workerCapacityForMode(process.env.AGLIO_WORKER_CAPACITY ?? 1, mode),
|
|
43451
|
+
claimTimeoutSeconds,
|
|
43365
43452
|
workspaceRoot,
|
|
43366
43453
|
hostWorkspaceRoot,
|
|
43367
43454
|
runnerImage,
|