aglio-worker 0.13.0 → 0.22.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 +219 -49
  2. package/package.json +1 -1
package/agent.js CHANGED
@@ -5052,8 +5052,8 @@ var require_multipleOf = __commonJS({
5052
5052
  const { gen, data, schemaCode, it } = cxt;
5053
5053
  const prec = it.opts.multipleOfPrecision;
5054
5054
  const res = gen.let("res");
5055
- const invalid = prec ? (0, codegen_1._)`Math.abs(Math.round(${res}) - ${res}) > 1e-${prec}` : (0, codegen_1._)`${res} !== parseInt(${res})`;
5056
- cxt.fail$data((0, codegen_1._)`(${schemaCode} === 0 || (${res} = ${data}/${schemaCode}, ${invalid}))`);
5055
+ const invalid2 = prec ? (0, codegen_1._)`Math.abs(Math.round(${res}) - ${res}) > 1e-${prec}` : (0, codegen_1._)`${res} !== parseInt(${res})`;
5056
+ cxt.fail$data((0, codegen_1._)`(${schemaCode} === 0 || (${res} = ${data}/${schemaCode}, ${invalid2}))`);
5057
5057
  }
5058
5058
  };
5059
5059
  exports.default = def;
@@ -27378,6 +27378,20 @@ var autoClaimScheduleSchema = external_exports.object({
27378
27378
  start: autoClaimTimeSchema.default("00:00"),
27379
27379
  end: autoClaimTimeSchema.default("00:00")
27380
27380
  }).default({ enabled: false, timezone: "UTC", days: [0, 1, 2, 3, 4, 5, 6], start: "00:00", end: "00:00" });
27381
+ var routingLabelsSchema = external_exports.array(external_exports.string()).max(16).superRefine((labels, ctx) => {
27382
+ try {
27383
+ validateRoutingLabels(labels);
27384
+ } catch (error62) {
27385
+ ctx.addIssue({
27386
+ code: "custom",
27387
+ message: error62 instanceof Error ? error62.message : "Invalid worker labels."
27388
+ });
27389
+ }
27390
+ });
27391
+ var cardRoutingOverrideSchema = external_exports.object({
27392
+ requiredWorkerLabels: routingLabelsSchema.nullable().optional(),
27393
+ planningRequiredWorkerLabels: routingLabelsSchema.nullable().optional()
27394
+ });
27381
27395
  var projectSettingsSchema = external_exports.object({
27382
27396
  /** Demo projects are sandbox rows: the worker never claims them and no model runs. */
27383
27397
  demoMode: external_exports.boolean().default(false),
@@ -27402,23 +27416,20 @@ var projectSettingsSchema = external_exports.object({
27402
27416
  runRetryBackoffSeconds: external_exports.number().int().min(10).max(600).default(60),
27403
27417
  runRetryMaxBackoffSeconds: external_exports.number().int().min(60).max(3600).default(600),
27404
27418
  notificationSound: external_exports.boolean().default(true),
27419
+ /** Null inherits the workspace default. True or false overrides it for this project. */
27420
+ requireVerificationEvidence: external_exports.boolean().nullable().default(null),
27405
27421
  /** Slack incoming webhook for question, failure and PR-ready alerts. Null disables Slack. */
27406
27422
  slackWebhookUrl: external_exports.string().trim().max(500).refine(isSlackWebhookUrl, "Slack webhook must be an https URL").nullable().default(null),
27407
27423
  /** Rendered column strip: built-in columns and custom list ids. */
27408
27424
  boardColumnOrder: external_exports.array(external_exports.string()).optional(),
27409
27425
  /** Last repository connectivity check from Project settings or the worker gate. */
27410
27426
  repoConnection: repoConnectionReportSchema.nullish(),
27411
- /** Worker labels every job in this project needs. Empty means any worker. */
27412
- requiredWorkerLabels: external_exports.array(external_exports.string()).max(16).default([]).superRefine((labels, ctx) => {
27413
- try {
27414
- validateRoutingLabels(labels);
27415
- } catch (error62) {
27416
- ctx.addIssue({
27417
- code: "custom",
27418
- message: error62 instanceof Error ? error62.message : "Invalid worker labels."
27419
- });
27420
- }
27421
- })
27427
+ /** Build, consult and review job labels. Empty means any worker. */
27428
+ requiredWorkerLabels: routingLabelsSchema.default([]),
27429
+ /** Planning job labels. Empty means any online worker, so planning never waits on a build machine. */
27430
+ planningRequiredWorkerLabels: routingLabelsSchema.default([]),
27431
+ /** Undefined inherits the workspace switch. True or false overrides it. */
27432
+ postBuildReply: external_exports.boolean().optional()
27422
27433
  });
27423
27434
  var repoConfigSchema = external_exports.object({
27424
27435
  id: external_exports.string(),
@@ -27671,6 +27682,18 @@ function artifactFileName(id, label) {
27671
27682
  return `${id}-${sanitizeFileName(label)}`;
27672
27683
  }
27673
27684
 
27685
+ // ../../packages/shared/src/feedback.ts
27686
+ var FEEDBACK_TYPES = ["bug", "feature"];
27687
+ var FEEDBACK_MAX_ATTACHMENT_BYTES = 10 * 1024 * 1024;
27688
+ var FEEDBACK_RATE_WINDOW_MS = 60 * 60 * 1e3;
27689
+ var feedbackTypeSchema = external_exports.enum(FEEDBACK_TYPES);
27690
+ var feedbackInputSchema = external_exports.object({
27691
+ type: feedbackTypeSchema,
27692
+ title: external_exports.string().trim().min(5, "Title must be at least 5 characters").max(140, "Title must be 140 characters or fewer"),
27693
+ description: external_exports.string().trim().min(10, "Description must be at least 10 characters").max(5e3, "Description must be 5000 characters or fewer"),
27694
+ pageUrl: external_exports.string().trim().max(500, "Page URL must be 500 characters or fewer").refine((value) => value === "" || URL.canParse(value), "Page URL must be a valid URL").optional().default("")
27695
+ });
27696
+
27674
27697
  // ../../packages/shared/src/providers.ts
27675
27698
  var apiKey = (env, drivers, label = "API key") => ({
27676
27699
  type: "api_key",
@@ -28152,10 +28175,6 @@ var cardLabelInputSchema = external_exports.object({
28152
28175
  var INVITE_TTL_DAYS = 7;
28153
28176
  var INVITE_TTL_MS = INVITE_TTL_DAYS * 24 * 60 * 60 * 1e3;
28154
28177
 
28155
- // ../../packages/shared/src/workspace-theme.ts
28156
- var WORKSPACE_THEME_ACCENT_IDS = ["default", "violet", "emerald", "amber", "rose"];
28157
- var WORKSPACE_THEME_SCALES = ["compact", "default", "large"];
28158
-
28159
28178
  // ../../packages/shared/src/workspace-settings.ts
28160
28179
  var WORKSPACE_SETTINGS_DEFAULTS = {
28161
28180
  allowMemberInvites: true,
@@ -28166,10 +28185,8 @@ var WORKSPACE_SETTINGS_DEFAULTS = {
28166
28185
  defaultGitProvider: "github",
28167
28186
  /** The shared workspace consultant is opt-out. Disabled hides every entry point. */
28168
28187
  consultantEnabled: true,
28169
- /** Workspace-wide accent preset. Applied to every member, light and dark. */
28170
- themeAccent: "default",
28171
- /** Workspace-wide text scale. Same for every member so boards stay aligned. */
28172
- themeScale: "default"
28188
+ /** Post the agent's exact final build message as a card comment. */
28189
+ postBuildReply: true
28173
28190
  };
28174
28191
  var workspaceSettingsSchema = external_exports.object({
28175
28192
  allowMemberInvites: external_exports.boolean().catch(WORKSPACE_SETTINGS_DEFAULTS.allowMemberInvites),
@@ -28178,10 +28195,21 @@ var workspaceSettingsSchema = external_exports.object({
28178
28195
  artifactRetentionDays: external_exports.number().int().min(0).max(3650).catch(WORKSPACE_SETTINGS_DEFAULTS.artifactRetentionDays),
28179
28196
  defaultGitProvider: external_exports.string().optional().transform((value) => normalizeGitProvider(value)).catch(WORKSPACE_SETTINGS_DEFAULTS.defaultGitProvider),
28180
28197
  consultantEnabled: external_exports.boolean().catch(WORKSPACE_SETTINGS_DEFAULTS.consultantEnabled),
28181
- themeAccent: external_exports.enum(WORKSPACE_THEME_ACCENT_IDS).catch(WORKSPACE_SETTINGS_DEFAULTS.themeAccent),
28182
- themeScale: external_exports.enum(WORKSPACE_THEME_SCALES).catch(WORKSPACE_SETTINGS_DEFAULTS.themeScale)
28198
+ postBuildReply: external_exports.boolean().catch(WORKSPACE_SETTINGS_DEFAULTS.postBuildReply)
28183
28199
  }).passthrough();
28184
28200
 
28201
+ // ../../packages/shared/src/theme.ts
28202
+ var THEME_ACCENT_IDS = ["default", "violet", "emerald", "amber", "rose"];
28203
+ var THEME_SCALES = ["compact", "default", "large"];
28204
+ var DEFAULT_THEME_PREFERENCES = {
28205
+ themeAccent: "default",
28206
+ themeScale: "default"
28207
+ };
28208
+ var themePreferencesSchema = external_exports.object({
28209
+ themeAccent: external_exports.enum(THEME_ACCENT_IDS).catch(DEFAULT_THEME_PREFERENCES.themeAccent),
28210
+ themeScale: external_exports.enum(THEME_SCALES).catch(DEFAULT_THEME_PREFERENCES.themeScale)
28211
+ });
28212
+
28185
28213
  // ../../packages/shared/src/plans.ts
28186
28214
  var PLAN_IDS = ["free", "team", "enterprise"];
28187
28215
  var MEGABYTE = 1024 * 1024;
@@ -28250,7 +28278,7 @@ var CARD_OPEN_SIZES = ["default", "large", "fullscreen"];
28250
28278
  var cardOpenSizeSchema = external_exports.enum(CARD_OPEN_SIZES);
28251
28279
 
28252
28280
  // src/remote-docker.ts
28253
- import { existsSync } from "node:fs";
28281
+ import { existsSync, readFileSync as readFileSync2 } from "node:fs";
28254
28282
  import { PassThrough } from "node:stream";
28255
28283
  import { join as join2, posix, relative, resolve } from "node:path";
28256
28284
  import Dockerode from "dockerode";
@@ -28355,15 +28383,137 @@ async function ensureDockerImage(input2) {
28355
28383
  return "pulled";
28356
28384
  }
28357
28385
 
28386
+ // src/resources.ts
28387
+ var GIB = 1024 * 1024 * 1024;
28388
+ var AUTO_MEMORY_RATIO = 0.8;
28389
+ var MEMORY_UNITS = {
28390
+ "": 1,
28391
+ b: 1,
28392
+ k: 1024,
28393
+ kb: 1024,
28394
+ m: 1024 ** 2,
28395
+ mb: 1024 ** 2,
28396
+ g: 1024 ** 3,
28397
+ gb: 1024 ** 3,
28398
+ t: 1024 ** 4,
28399
+ tb: 1024 ** 4
28400
+ };
28401
+ var AGENT_RESOURCE_FALLBACK = {
28402
+ cpu: 2,
28403
+ memoryBytes: 4 * GIB
28404
+ };
28405
+ function invalid(variable, value, expected) {
28406
+ return new Error(`Invalid ${variable} value "${value}": expected ${expected}`);
28407
+ }
28408
+ function parseCpuLimit(value, variable = "AGENT_CPU_LIMIT") {
28409
+ const trimmed = value.trim();
28410
+ if (!/^\d+(?:\.\d+)?$/.test(trimmed)) {
28411
+ throw invalid(variable, value, "positive CPU cores such as 2 or 2.5");
28412
+ }
28413
+ const cores = Number(trimmed);
28414
+ if (!Number.isFinite(cores) || cores <= 0) {
28415
+ throw invalid(variable, value, "positive CPU cores such as 2 or 2.5");
28416
+ }
28417
+ return cores;
28418
+ }
28419
+ function parseMemoryLimit(value, variable = "AGENT_MEMORY_LIMIT") {
28420
+ const trimmed = value.trim().toLowerCase();
28421
+ const match = /^(\d+(?:\.\d+)?)\s*([bkmgt]?b?)$/.exec(trimmed);
28422
+ const multiplier = match ? MEMORY_UNITS[match[2] ?? ""] : void 0;
28423
+ const amount = match ? Number(match[1]) : Number.NaN;
28424
+ if (!match || multiplier === void 0 || !Number.isFinite(amount)) {
28425
+ throw invalid(variable, value, "bytes or a Docker-style size such as 512m, 8g, 1073741824");
28426
+ }
28427
+ const bytes = Math.round(amount * multiplier);
28428
+ if (bytes <= 0) {
28429
+ throw invalid(variable, value, "bytes or a Docker-style size such as 512m, 8g, 1073741824");
28430
+ }
28431
+ return bytes;
28432
+ }
28433
+ function cpuToNanoCpus(cpu) {
28434
+ return Math.round(cpu * 1e9);
28435
+ }
28436
+ function readAgentResourceOverrides(source = process.env) {
28437
+ const cpu = source.AGENT_CPU_LIMIT?.trim();
28438
+ const memory = source.AGENT_MEMORY_LIMIT?.trim();
28439
+ return {
28440
+ cpu: cpu ? parseCpuLimit(cpu) : null,
28441
+ memory: memory ? parseMemoryLimit(memory) : null
28442
+ };
28443
+ }
28444
+ function resolveAgentResources(input2) {
28445
+ const fallback = input2.fallback ?? AGENT_RESOURCE_FALLBACK;
28446
+ const hostCpu = input2.host?.cpuCount;
28447
+ const hostMemory = input2.host?.totalMemoryBytes;
28448
+ let cpu;
28449
+ let cpuSource;
28450
+ if (input2.cpu != null) {
28451
+ cpu = input2.cpu;
28452
+ cpuSource = "env";
28453
+ } else if (typeof hostCpu === "number" && Number.isFinite(hostCpu) && hostCpu > 0) {
28454
+ cpu = hostCpu;
28455
+ cpuSource = "auto";
28456
+ } else {
28457
+ cpu = fallback.cpu;
28458
+ cpuSource = "fallback";
28459
+ }
28460
+ let memoryBytes;
28461
+ let memorySource;
28462
+ if (input2.memory != null) {
28463
+ memoryBytes = input2.memory;
28464
+ memorySource = "env";
28465
+ } else {
28466
+ const detected = typeof hostMemory === "number" && Number.isFinite(hostMemory) && hostMemory > 0 ? Math.floor(hostMemory * AUTO_MEMORY_RATIO) : 0;
28467
+ if (detected > 0) {
28468
+ memoryBytes = detected;
28469
+ memorySource = "auto";
28470
+ } else {
28471
+ memoryBytes = fallback.memoryBytes;
28472
+ memorySource = "fallback";
28473
+ }
28474
+ }
28475
+ return { cpu, memoryBytes, nanoCpus: cpuToNanoCpus(cpu), cpuSource, memorySource };
28476
+ }
28477
+ function agentHostResources(resources) {
28478
+ return { Memory: resources.memoryBytes, NanoCpus: resources.nanoCpus };
28479
+ }
28480
+
28358
28481
  // src/remote-docker.ts
28359
- async function runnerCpuCount(client) {
28482
+ async function resolveRemoteAgentResources(client, log, overrides = readAgentResourceOverrides()) {
28483
+ const needsDetection = overrides.cpu == null || overrides.memory == null;
28484
+ const info = needsDetection ? await client.info().catch(() => null) : null;
28485
+ const resources = resolveAgentResources({
28486
+ cpu: overrides.cpu,
28487
+ memory: overrides.memory,
28488
+ host: { cpuCount: info?.NCPU, totalMemoryBytes: info?.MemTotal }
28489
+ });
28490
+ if (resources.cpuSource === "fallback" || resources.memorySource === "fallback") {
28491
+ log?.("remote daemon resources unavailable, using fallback 2 CPU / 4 GB", {
28492
+ cpuSource: resources.cpuSource,
28493
+ memorySource: resources.memorySource
28494
+ });
28495
+ }
28496
+ return resources;
28497
+ }
28498
+ async function loginCpuNanoCpus(client) {
28360
28499
  const info = await client.info().catch(() => null);
28361
- return Math.max(1, Math.min(2, Math.floor(info?.NCPU ?? 1)));
28500
+ return Math.max(1, Math.min(2, Math.floor(info?.NCPU ?? 1))) * 1e9;
28362
28501
  }
28363
28502
  var CONTAINER_WORKSPACE = "/workspace";
28364
28503
  function isRunningInContainer() {
28365
28504
  return existsSync("/.dockerenv") || existsSync("/run/.containerenv");
28366
28505
  }
28506
+ function parseAgentContainerId(mountInfo) {
28507
+ const match = mountInfo.match(/containers\/([0-9a-f]{64})\/(?:hostname|hosts|resolv\.conf)/);
28508
+ return match?.[1] ?? null;
28509
+ }
28510
+ function readAgentContainerId() {
28511
+ try {
28512
+ return parseAgentContainerId(readFileSync2("/proc/self/mountinfo", "utf8"));
28513
+ } catch {
28514
+ return null;
28515
+ }
28516
+ }
28367
28517
  function resolveRunnerNetworkMode(input2) {
28368
28518
  const override = input2.override?.trim();
28369
28519
  if (override) return override;
@@ -28405,7 +28555,7 @@ function sleep(ms) {
28405
28555
  return new Promise((resolvePromise) => setTimeout(resolvePromise, ms));
28406
28556
  }
28407
28557
  var defaultClients = /* @__PURE__ */ new Map();
28408
- function defaultClient(socketPath) {
28558
+ function remoteDockerClient(socketPath) {
28409
28559
  const existing = defaultClients.get(socketPath);
28410
28560
  if (existing) return existing;
28411
28561
  const client = new Dockerode({ socketPath });
@@ -28413,7 +28563,7 @@ function defaultClient(socketPath) {
28413
28563
  return client;
28414
28564
  }
28415
28565
  async function createDockerJobExecutor(input2) {
28416
- const client = input2.client ?? defaultClient(input2.socketPath ?? "/var/run/docker.sock");
28566
+ const client = input2.client ?? remoteDockerClient(input2.socketPath ?? "/var/run/docker.sock");
28417
28567
  await ensureDockerImage({ client, image: input2.image, log: input2.log });
28418
28568
  const { hostPath } = mapDockerWorkspace({
28419
28569
  workspaceRoot: input2.workspaceRoot,
@@ -28437,11 +28587,11 @@ async function createDockerJobExecutor(input2) {
28437
28587
  if (error62.statusCode !== 404) throw error62;
28438
28588
  }
28439
28589
  const networkMode = input2.networkMode?.trim() || "bridge";
28590
+ const resources = input2.resources ?? await resolveRemoteAgentResources(client, input2.log);
28440
28591
  const hostConfig = {
28441
28592
  Binds: [`${hostPath}:${CONTAINER_WORKSPACE}`],
28442
28593
  NetworkMode: networkMode,
28443
- Memory: 4 * 1024 * 1024 * 1024,
28444
- NanoCpus: await runnerCpuCount(client) * 1e9,
28594
+ ...agentHostResources(resources),
28445
28595
  PidsLimit: 1024,
28446
28596
  AutoRemove: false,
28447
28597
  Init: true,
@@ -28527,7 +28677,7 @@ async function createDockerJobExecutor(input2) {
28527
28677
  };
28528
28678
  }
28529
28679
  async function prefetchRunnerImage(input2) {
28530
- const client = input2.client ?? defaultClient(input2.socketPath ?? "/var/run/docker.sock");
28680
+ const client = input2.client ?? remoteDockerClient(input2.socketPath ?? "/var/run/docker.sock");
28531
28681
  try {
28532
28682
  if (isRegistryImage(input2.image)) {
28533
28683
  await pullDockerImage(client, input2.image);
@@ -28550,7 +28700,7 @@ async function prefetchRunnerImage(input2) {
28550
28700
  }
28551
28701
  }
28552
28702
  async function createDockerLoginContainer(input2) {
28553
- const client = new Dockerode({ socketPath: input2.socketPath ?? "/var/run/docker.sock" });
28703
+ const client = input2.client ?? new Dockerode({ socketPath: input2.socketPath ?? "/var/run/docker.sock" });
28554
28704
  await ensureDockerImage({
28555
28705
  client,
28556
28706
  image: input2.image
@@ -28576,6 +28726,7 @@ async function createDockerLoginContainer(input2) {
28576
28726
  } catch (error62) {
28577
28727
  if (error62.statusCode !== 404) throw error62;
28578
28728
  }
28729
+ const overrides = readAgentResourceOverrides();
28579
28730
  const container = await client.createContainer({
28580
28731
  Image: input2.image,
28581
28732
  name,
@@ -28588,8 +28739,10 @@ async function createDockerLoginContainer(input2) {
28588
28739
  Binds: [`${hostPath}:${CONTAINER_WORKSPACE}`],
28589
28740
  NetworkMode: "bridge",
28590
28741
  ExtraHosts: input2.extraHosts ?? [],
28591
- Memory: 2 * 1024 * 1024 * 1024,
28592
- NanoCpus: await runnerCpuCount(client) * 1e9,
28742
+ // Login keeps its small legacy default and never auto-detects, but an
28743
+ // explicit AGENT_CPU_LIMIT / AGENT_MEMORY_LIMIT override wins.
28744
+ Memory: overrides.memory ?? 2 * 1024 * 1024 * 1024,
28745
+ NanoCpus: overrides.cpu != null ? cpuToNanoCpus(overrides.cpu) : await loginCpuNanoCpus(client),
28593
28746
  PidsLimit: 512,
28594
28747
  AutoRemove: false,
28595
28748
  Init: true,
@@ -28682,11 +28835,11 @@ async function verifyHostRuntime(input2, run = defaultRunner) {
28682
28835
  import { statfs } from "node:fs/promises";
28683
28836
  import { dirname, resolve as resolve2 } from "node:path";
28684
28837
  import Dockerode2 from "dockerode";
28685
- var GIB = 1024 * 1024 * 1024;
28838
+ var GIB2 = 1024 * 1024 * 1024;
28686
28839
  var DISK_GUARD_DEFAULTS = {
28687
28840
  enabled: true,
28688
- minFreeBytes: 10 * GIB,
28689
- criticalFreeBytes: 2 * GIB,
28841
+ minFreeBytes: 10 * GIB2,
28842
+ criticalFreeBytes: 2 * GIB2,
28690
28843
  buildCachePrune: true,
28691
28844
  runnerImageRetentionDays: 14,
28692
28845
  intervalMs: 60 * 60 * 1e3
@@ -28708,16 +28861,16 @@ function resolveDiskGuardPolicy(source = process.env) {
28708
28861
  const minFreeBytes = numberFromEnv(
28709
28862
  source,
28710
28863
  "AGLIO_DISK_MIN_FREE_GB",
28711
- DISK_GUARD_DEFAULTS.minFreeBytes / GIB,
28864
+ DISK_GUARD_DEFAULTS.minFreeBytes / GIB2,
28712
28865
  0
28713
- ) * GIB;
28866
+ ) * GIB2;
28714
28867
  const criticalFreeBytes = Math.min(
28715
28868
  numberFromEnv(
28716
28869
  source,
28717
28870
  "AGLIO_DISK_CRITICAL_FREE_GB",
28718
- DISK_GUARD_DEFAULTS.criticalFreeBytes / GIB,
28871
+ DISK_GUARD_DEFAULTS.criticalFreeBytes / GIB2,
28719
28872
  0
28720
- ) * GIB,
28873
+ ) * GIB2,
28721
28874
  // A critical threshold above the low threshold would make "low" unreachable.
28722
28875
  minFreeBytes
28723
28876
  );
@@ -28888,7 +29041,7 @@ async function cleanupDockerDisk(input2) {
28888
29041
  input2.log?.("docker disk cleanup", {
28889
29042
  status: result.status,
28890
29043
  pressure,
28891
- freeGb: input2.disk ? Number((input2.disk.freeBytes / GIB).toFixed(1)) : null,
29044
+ freeGb: input2.disk ? Number((input2.disk.freeBytes / GIB2).toFixed(1)) : null,
28892
29045
  reclaimedMb: Math.round(result.spaceReclaimedBytes / (1024 * 1024)),
28893
29046
  danglingImages: result.danglingImagesRemoved,
28894
29047
  runnerImages: result.runnerImagesRemoved,
@@ -30354,6 +30507,7 @@ async function runRemoteCardJob(deps) {
30354
30507
  });
30355
30508
  const exit = await runAgent(command, env);
30356
30509
  if (deps.cancelled()) return { state: "cancelled", error: "Cancelled from the board" };
30510
+ await status({ finalMessage: exit.finalMessage });
30357
30511
  if (exit.error) return { state: "failed", error: exit.error };
30358
30512
  if (exit.sessionId) await status({ sessionId: exit.sessionId, usage: exit.usage });
30359
30513
  if (context.card && (context.run.kind === "build" || context.run.kind === "consult")) {
@@ -30664,6 +30818,7 @@ async function runRemoteCardJob(deps) {
30664
30818
  let messageSeq = 0;
30665
30819
  let lastFlushAt = 0;
30666
30820
  let chatMessageId = null;
30821
+ let finalMessage = "";
30667
30822
  let toolCalls = 0;
30668
30823
  let consumeQueue = Promise.resolve();
30669
30824
  const isPlanning = context.run.kind === "planning";
@@ -30749,10 +30904,12 @@ async function runRemoteCardJob(deps) {
30749
30904
  }
30750
30905
  if (parsed.messageEnd) {
30751
30906
  await flushAssistant(true);
30907
+ finalMessage = text || messageText;
30752
30908
  messageText = "";
30753
30909
  messageSeq += 1;
30754
30910
  chatMessageId = null;
30755
30911
  } else if (!parsed.assistantDelta) {
30912
+ finalMessage = text;
30756
30913
  messageSeq += 1;
30757
30914
  chatMessageId = null;
30758
30915
  }
@@ -30807,18 +30964,20 @@ async function runRemoteCardJob(deps) {
30807
30964
  });
30808
30965
  await consumeQueue;
30809
30966
  await flushAssistant(true);
30967
+ if (!finalMessage && messageText) finalMessage = messageText;
30810
30968
  clearTimeout(timeout);
30811
30969
  deps.signal.removeEventListener("abort", forwardAbort);
30812
- if (deps.cancelled()) return { error: null, sessionId, usage: usagePatch };
30813
- if (error62) return { error: error62, sessionId, usage: usagePatch };
30970
+ if (deps.cancelled()) return { error: null, sessionId, finalMessage, usage: usagePatch };
30971
+ if (error62) return { error: error62, sessionId, finalMessage, usage: usagePatch };
30814
30972
  if (code !== 0 && !final && !timedOut) {
30815
30973
  return {
30816
30974
  error: stderr.trim().slice(-2e3) || `Agent exited with code ${code}`,
30817
30975
  sessionId,
30976
+ finalMessage,
30818
30977
  usage: usagePatch
30819
30978
  };
30820
30979
  }
30821
- return { error: null, sessionId, usage: usagePatch };
30980
+ return { error: null, sessionId, finalMessage, usage: usagePatch };
30822
30981
  }
30823
30982
  }
30824
30983
 
@@ -43583,7 +43742,7 @@ function createServerForRun(run) {
43583
43742
  server.registerTool(
43584
43743
  "verification_record",
43585
43744
  {
43586
- description: "Record mandatory verification evidence before opening a PR. Non-N/A kinds require an artifactId. N/A requires a reason.",
43745
+ description: "Record verification evidence before opening a PR. Non-N/A kinds require an artifactId unless this project or its workspace made evidence optional. N/A requires a reason.",
43587
43746
  inputSchema: {
43588
43747
  kind: external_exports.enum(VERIFICATION_KINDS),
43589
43748
  summary: external_exports.string().min(1).max(400),
@@ -43660,7 +43819,7 @@ function createServerForRun(run) {
43660
43819
  server.registerTool(
43661
43820
  "pr_open",
43662
43821
  {
43663
- description: "Push the branch and open a pull request for one repo. Requires verification evidence unless the workspace relaxed the rule. Moves the card to review.",
43822
+ description: "Push the branch and open a pull request for one repo. Requires verification evidence unless this project or its workspace made evidence optional. Moves the card to review.",
43664
43823
  inputSchema: {
43665
43824
  repoId: external_exports.string().min(1),
43666
43825
  branch: external_exports.string().min(1),
@@ -43786,10 +43945,11 @@ async function main() {
43786
43945
  const mcpPort = Number(process.env.AGLIO_WORKER_MCP_PORT ?? 4310);
43787
43946
  const claimTimeoutRaw = Number(process.env.AGLIO_WORKER_CLAIM_TIMEOUT_SECONDS);
43788
43947
  const claimTimeoutSeconds = Number.isFinite(claimTimeoutRaw) && claimTimeoutRaw > 0 ? claimTimeoutRaw : void 0;
43789
- const agentContainerId = process.env.HOSTNAME?.trim() || hostname3();
43948
+ const containerized = isRunningInContainer();
43949
+ const agentContainerId = (containerized ? readAgentContainerId() : null) || process.env.HOSTNAME?.trim() || hostname3();
43790
43950
  const runnerNetwork = mode === "docker" ? resolveRunnerNetworkMode({
43791
43951
  override: process.env.AGLIO_WORKER_RUNNER_NETWORK,
43792
- containerized: isRunningInContainer(),
43952
+ containerized,
43793
43953
  hostname: agentContainerId
43794
43954
  }) : "host";
43795
43955
  const containerMcpUrl = runnerMcpUrl({
@@ -43835,6 +43995,15 @@ async function main() {
43835
43995
  );
43836
43996
  const hostTools = await verifyHostRuntime({ mode, labels, platform: process.platform });
43837
43997
  if (hostTools.length > 0) console.log(`[worker-agent] host tools ${hostTools.join(" | ")}`);
43998
+ const agentResources = mode === "docker" ? await resolveRemoteAgentResources(
43999
+ remoteDockerClient(dockerSocket),
44000
+ (message, extra) => console.log(`[worker-agent] ${message}`, extra ?? "")
44001
+ ) : void 0;
44002
+ if (agentResources) {
44003
+ console.log(
44004
+ `[worker-agent] agent resources cpu=${agentResources.cpu} (${agentResources.cpuSource}) memoryBytes=${agentResources.memoryBytes} (${agentResources.memorySource})`
44005
+ );
44006
+ }
43838
44007
  if (mode === "docker") {
43839
44008
  void prefetchRunnerImage({
43840
44009
  image: runnerImage,
@@ -43858,6 +44027,7 @@ async function main() {
43858
44027
  // Only the bridge fallback needs a host gateway. Docker Desktop
43859
44028
  // resolves host.docker.internal already; Linux needs the entry.
43860
44029
  extraHosts: runnerExtraHosts,
44030
+ resources: agentResources,
43861
44031
  log: (message, extra) => console.log(`[worker-agent] ${message}`, extra ?? "")
43862
44032
  }) : void 0
43863
44033
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aglio-worker",
3
- "version": "0.13.0",
3
+ "version": "0.22.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": {