aglio-worker 0.1.0 → 0.8.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.
Files changed (2) hide show
  1. package/agent.js +156 -65
  2. package/package.json +2 -2
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", "node"];
27275
- var ROUTING_SIZE_VALUES = ["small", "large"];
27276
- var ROUTING_LABEL_VALUES = {
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
- var LABEL_PATTERN = /^([a-z]+):([a-z0-9][a-z0-9._-]*)$/;
27283
- function parseOne(value) {
27284
- const match = LABEL_PATTERN.exec(value);
27285
- if (!match) {
27286
- throw new Error(`Invalid routing label "${value}". Use key:value, for example mode:host.`);
27287
- }
27288
- const key = match[1];
27289
- if (!ROUTING_LABEL_KEYS.includes(key)) {
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().toLowerCase();
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
- const { key, value } = parseOne(label);
27321
- if (key === "os" || key === "mode" || key === "size") {
27322
- const previous = seenValues.get(key);
27323
- if (previous && previous !== value) {
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
- seenValues.set(key, value);
27328
- if (key === "tools") tools.add(value);
27329
- }
27330
- const os = seenValues.get("os");
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 = 64;
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_";
@@ -28138,6 +28148,10 @@ var cardLabelInputSchema = external_exports.object({
28138
28148
  color: cardLabelColorSchema.default(DEFAULT_CARD_LABEL_COLOR)
28139
28149
  });
28140
28150
 
28151
+ // ../../packages/shared/src/workspace.ts
28152
+ var INVITE_TTL_DAYS = 7;
28153
+ var INVITE_TTL_MS = INVITE_TTL_DAYS * 24 * 60 * 60 * 1e3;
28154
+
28141
28155
  // ../../packages/shared/src/workspace-theme.ts
28142
28156
  var WORKSPACE_THEME_ACCENT_IDS = ["default", "violet", "emerald", "amber", "rose"];
28143
28157
  var WORKSPACE_THEME_SCALES = ["compact", "default", "large"];
@@ -30748,15 +30762,38 @@ function parseClaimedJob(value) {
30748
30762
  if (!lease || typeof lease.id !== "string" || typeof lease.token !== "string") return null;
30749
30763
  return value;
30750
30764
  }
30765
+ function claimHoldSeconds(config2) {
30766
+ const requested = config2.claimTimeoutSeconds;
30767
+ if (typeof requested !== "number" || !Number.isFinite(requested)) {
30768
+ return WORKER_CLAIM_LONG_POLL_SECONDS;
30769
+ }
30770
+ return Math.min(Math.max(Math.round(requested), 5), 120);
30771
+ }
30772
+ var CLAIM_REQUEST_GRACE_MS = 2e4;
30751
30773
  async function claim2(config2, token, fetchImpl, signal) {
30752
- const response = await request(
30753
- config2,
30754
- fetchImpl,
30755
- "/api/workers/claim",
30756
- token,
30757
- { version: config2.version, capacity: config2.capacity },
30758
- signal
30774
+ const holdSeconds = claimHoldSeconds(config2);
30775
+ const controller = new AbortController();
30776
+ const onAbort = () => controller.abort(signal?.reason);
30777
+ signal?.addEventListener("abort", onAbort, { once: true });
30778
+ const timer = setTimeout(
30779
+ () => controller.abort(new Error("Claim request did not answer in time.")),
30780
+ holdSeconds * 1e3 + CLAIM_REQUEST_GRACE_MS
30759
30781
  );
30782
+ timer.unref?.();
30783
+ let response;
30784
+ try {
30785
+ response = await request(
30786
+ config2,
30787
+ fetchImpl,
30788
+ "/api/workers/claim",
30789
+ token,
30790
+ { version: config2.version, capacity: config2.capacity, claimTimeoutSeconds: holdSeconds },
30791
+ controller.signal
30792
+ );
30793
+ } finally {
30794
+ clearTimeout(timer);
30795
+ signal?.removeEventListener("abort", onAbort);
30796
+ }
30760
30797
  if (response.status === 204) return { status: 204, job: null, reason: null, message: null };
30761
30798
  if (!response.ok) throw await errorFrom(response);
30762
30799
  const body = await responseBody(response);
@@ -30767,6 +30804,15 @@ async function claim2(config2, token, fetchImpl, signal) {
30767
30804
  message: typeof body.message === "string" ? body.message : null
30768
30805
  };
30769
30806
  }
30807
+ function isTransientControlPlaneError(error62) {
30808
+ if (error62 instanceof WorkerHttpError) return error62.status >= 500 || error62.status === 429;
30809
+ return true;
30810
+ }
30811
+ var CLAIM_BACKOFF_MS = [5e3, 1e4, 2e4, 3e4];
30812
+ function claimBackoffMs(attempt) {
30813
+ const index = Math.min(Math.max(Math.floor(attempt), 1), CLAIM_BACKOFF_MS.length) - 1;
30814
+ return CLAIM_BACKOFF_MS[index];
30815
+ }
30770
30816
  async function jobTokenRequest(config2, fetchImpl, path, token, init) {
30771
30817
  return fetchImpl(joinUrl(config2.apiUrl, path), {
30772
30818
  method: init.method,
@@ -30805,18 +30851,23 @@ function workerCardApi(config2, fetchImpl, signal) {
30805
30851
  return new Uint8Array(await response.arrayBuffer());
30806
30852
  },
30807
30853
  async updateRunStatus(input2) {
30808
- const response = await jobTokenRequest(
30809
- config2,
30810
- fetchImpl,
30811
- `/api/workers/jobs/${input2.jobId}/run/status`,
30812
- input2.token,
30813
- {
30814
- method: "POST",
30815
- body: JSON.stringify({ leaseId: input2.leaseId, ...input2.patch }),
30816
- signal
30817
- }
30854
+ await retryTransient(
30855
+ async () => {
30856
+ const response = await jobTokenRequest(
30857
+ config2,
30858
+ fetchImpl,
30859
+ `/api/workers/jobs/${input2.jobId}/run/status`,
30860
+ input2.token,
30861
+ {
30862
+ method: "POST",
30863
+ body: JSON.stringify({ leaseId: input2.leaseId, ...input2.patch }),
30864
+ signal
30865
+ }
30866
+ );
30867
+ if (!response.ok) throw await errorFrom(response);
30868
+ },
30869
+ { signal }
30818
30870
  );
30819
- if (!response.ok) throw await errorFrom(response);
30820
30871
  },
30821
30872
  async appendRunEvents(input2) {
30822
30873
  const response = await jobTokenRequest(
@@ -31004,6 +31055,24 @@ function jobApi(config2, fetchImpl, signal) {
31004
31055
  }
31005
31056
  };
31006
31057
  }
31058
+ var TRANSIENT_RETRY_DELAYS_MS = [500, 1500];
31059
+ async function retryTransient(call, options = {}) {
31060
+ const sleep2 = options.sleep ?? ((ms) => new Promise((resolve6) => setTimeout(resolve6, ms)));
31061
+ let lastError;
31062
+ for (let attempt = 0; attempt <= TRANSIENT_RETRY_DELAYS_MS.length; attempt += 1) {
31063
+ try {
31064
+ return await call();
31065
+ } catch (error62) {
31066
+ lastError = error62;
31067
+ if (!isTransientControlPlaneError(error62) || options.signal?.aborted) throw error62;
31068
+ const delay = TRANSIENT_RETRY_DELAYS_MS[attempt];
31069
+ if (delay === void 0) break;
31070
+ options.onRetry?.(error62, attempt + 1);
31071
+ await sleep2(delay);
31072
+ }
31073
+ }
31074
+ throw lastError;
31075
+ }
31007
31076
  function fatalExitCode(error62) {
31008
31077
  if (error62.status === 401 && error62.code === "worker_revoked") return 0;
31009
31078
  if (error62.status === 426 || error62.code === "incompatible_worker_version") return 2;
@@ -31121,10 +31190,27 @@ async function runRemoteWorker(config2, deps = {}) {
31121
31190
  1,
31122
31191
  Math.min(workerCapacity(config2.capacity, resolved.capacity), workerCapacity(resolved.capacity))
31123
31192
  );
31193
+ let claimAttempt = 0;
31194
+ const backOffClaim = async (error62) => {
31195
+ claimAttempt += 1;
31196
+ const delay = claimBackoffMs(claimAttempt);
31197
+ if (claimAttempt === 1) {
31198
+ log("claim unavailable, backing off", {
31199
+ ...error62 instanceof WorkerHttpError ? { status: error62.status, code: error62.code } : {},
31200
+ error: error62 instanceof Error ? error62.message : String(error62),
31201
+ retryInSeconds: Math.round(delay / 1e3)
31202
+ });
31203
+ }
31204
+ await sleep2(delay);
31205
+ };
31124
31206
  const runSlot = async () => {
31125
31207
  while (!controller.signal.aborted) {
31126
31208
  try {
31127
31209
  const result = await claim2(config2, resolved.token, fetchImpl, controller.signal);
31210
+ if (claimAttempt > 0) {
31211
+ log("claim recovered", { attempts: claimAttempt });
31212
+ claimAttempt = 0;
31213
+ }
31128
31214
  if (result.status === 204) {
31129
31215
  lastReason = null;
31130
31216
  continue;
@@ -31194,11 +31280,13 @@ async function runRemoteWorker(config2, deps = {}) {
31194
31280
  }
31195
31281
  } catch (error62) {
31196
31282
  if (controller.signal.aborted) break;
31283
+ if (isTransientControlPlaneError(error62)) {
31284
+ await backOffClaim(error62);
31285
+ continue;
31286
+ }
31197
31287
  if (error62 instanceof WorkerHttpError) {
31198
31288
  fail2(error62, "claim");
31199
31289
  if (controller.signal.aborted) break;
31200
- } else {
31201
- log("claim failed", { error: error62 instanceof Error ? error62.message : String(error62) });
31202
31290
  }
31203
31291
  await sleep2(5e3);
31204
31292
  }
@@ -43330,6 +43418,8 @@ async function main() {
43330
43418
  const runnerImage = defaultRunnerImage();
43331
43419
  const dockerSocket = process.env.AGLIO_WORKER_DOCKER_SOCKET?.trim() || "/var/run/docker.sock";
43332
43420
  const mcpPort = Number(process.env.AGLIO_WORKER_MCP_PORT ?? 4310);
43421
+ const claimTimeoutRaw = Number(process.env.AGLIO_WORKER_CLAIM_TIMEOUT_SECONDS);
43422
+ const claimTimeoutSeconds = Number.isFinite(claimTimeoutRaw) && claimTimeoutRaw > 0 ? claimTimeoutRaw : void 0;
43333
43423
  const agentContainerId = process.env.HOSTNAME?.trim() || hostname3();
43334
43424
  const runnerNetwork = mode === "docker" ? resolveRunnerNetworkMode({
43335
43425
  override: process.env.AGLIO_WORKER_RUNNER_NETWORK,
@@ -43362,6 +43452,7 @@ async function main() {
43362
43452
  stateFile,
43363
43453
  // Simulators and Xcode share one signing context on a Mac host.
43364
43454
  capacity: workerCapacityForMode(process.env.AGLIO_WORKER_CAPACITY ?? 1, mode),
43455
+ claimTimeoutSeconds,
43365
43456
  workspaceRoot,
43366
43457
  hostWorkspaceRoot,
43367
43458
  runnerImage,
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "aglio-worker",
3
- "version": "0.1.0",
3
+ "version": "0.8.0",
4
4
  "description": "Host machine agent for Aglio: claims jobs over the API and starts aglio-runner containers.",
5
5
  "type": "module",
6
6
  "bin": {
7
- "aglio-worker": "./agent.js"
7
+ "aglio-worker": "agent.js"
8
8
  },
9
9
  "files": [
10
10
  "agent.js"