@mcoda/mswarm 0.1.106 → 0.1.108

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/dist/runtime.js CHANGED
@@ -4,6 +4,7 @@ import { cpus, freemem, hostname, homedir, loadavg, platform, totalmem, userInfo
4
4
  import { spawn } from "node:child_process";
5
5
  import { createHash, createHmac, randomBytes, randomUUID } from "node:crypto";
6
6
  import { isIP } from "node:net";
7
+ import { Agent, fetch as undiciFetch } from "undici";
7
8
  import { GlobalRepository } from "@mcoda/db";
8
9
  import { MswarmCodaliExecutor } from "./codali-executor.js";
9
10
  import { GENERATIVE_OPERATION_DEFAULT_PATHS, GENERATIVE_REQUEST_PARAMETER_ALLOWLISTS, MSWARM_CAPABILITY_SCHEMA_VERSION, CryptoHelper, assertMswarmSafeRelativePath, validateMswarmArchiveEntry, buildMswarmCapabilityNames, buildMswarmPrivateCapabilityCatalogEntry, buildMswarmLocalArtifactUri, buildMswarmSandboxProfile, defaultMswarmArtifactAccessPolicy, defaultMswarmArtifactRetentionPolicy, normalizeGenerativeModalities, normalizeSelfHostedGenerativeOperation, projectMswarmPublicCapabilities, validateMswarmGenericJobRequest } from "@mcoda/shared";
@@ -61,6 +62,7 @@ const DEFAULT_HEARTBEAT_INTERVAL_SECONDS = 30;
61
62
  const DEFAULT_SELF_HOSTED_NODE_VERSION = "0.1.70";
62
63
  const DEFAULT_REQUEST_TIMEOUT_MS = 10000;
63
64
  const DEFAULT_JOB_TIMEOUT_MS = 3600000;
65
+ const DEFAULT_RESULT_POST_TIMEOUT_MS = 5 * 60000;
64
66
  const DEFAULT_SERVICE_COMMAND_TIMEOUT_MS = 60000;
65
67
  const DEFAULT_CAPABILITY_PROBE_TIMEOUT_MS = 2000;
66
68
  const SELF_HOSTED_RUNTIME_PROTOCOL_VERSION = 1;
@@ -73,6 +75,12 @@ const DEFAULT_MCODA_LIST_ARGS = ["agent", "list", "--json", "--refresh-health"];
73
75
  const DEFAULT_COMMAND_MAX_BUFFER = 16 * 1024 * 1024;
74
76
  const DEFAULT_LOCAL_ARTIFACT_MAX_BYTES = 512 * 1024 * 1024;
75
77
  const DEFAULT_JOB_POLL_WAIT_MS = 2000;
78
+ const RESERVED_JOB_POLL_WAIT_MS = 250;
79
+ const MAX_POLL_EXCLUDED_AGENT_SLUGS = 64;
80
+ const MAX_RELAY_AGENT_SLUG_LENGTH = 255;
81
+ const DEFAULT_RESERVED_PRIORITY_MAX = -1;
82
+ const MIN_RELAY_PRIORITY = -100;
83
+ const MAX_RELAY_PRIORITY = 100;
76
84
  const JOB_POLL_RETRY_BASE_MS = 1000;
77
85
  const JOB_POLL_RETRY_MAX_MS = 60000;
78
86
  /**
@@ -283,6 +291,33 @@ const LOCAL_RUNNER_RESPONSE_FORMAT_ALIASES = {
283
291
  function optionalText(value) {
284
292
  return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
285
293
  }
294
+ function relayJobAgentAliases(job) {
295
+ const aliases = [job.agent_slug, job.source_agent_slug, job.remote_slug]
296
+ .map(optionalText)
297
+ .map((value) => value
298
+ ?.toLowerCase()
299
+ .replace(/[^a-z0-9]+/g, "-")
300
+ .replace(/-+/g, "-")
301
+ .replace(/^-|-$/g, "") || null)
302
+ .filter((value) => typeof value === "string" && value.length > 0 && value.length <= MAX_RELAY_AGENT_SLUG_LENGTH);
303
+ return [...new Set(aliases)];
304
+ }
305
+ function boundedRelayAgentAliases(activeAliases) {
306
+ const aliases = [];
307
+ const seen = new Set();
308
+ for (const group of activeAliases) {
309
+ for (const alias of group) {
310
+ if (seen.has(alias))
311
+ continue;
312
+ seen.add(alias);
313
+ aliases.push(alias);
314
+ if (aliases.length >= MAX_POLL_EXCLUDED_AGENT_SLUGS) {
315
+ return aliases;
316
+ }
317
+ }
318
+ }
319
+ return aliases;
320
+ }
286
321
  function isRecord(value) {
287
322
  return typeof value === "object" && value !== null && !Array.isArray(value);
288
323
  }
@@ -587,6 +622,39 @@ function parsePositiveInteger(value, fallback) {
587
622
  }
588
623
  return Math.floor(parsed);
589
624
  }
625
+ function parseNonNegativeInteger(value, fallback) {
626
+ const parsed = value === undefined || value === null || value === ""
627
+ ? fallback
628
+ : typeof value === "number"
629
+ ? value
630
+ : typeof value === "string"
631
+ ? Number(value)
632
+ : Number.NaN;
633
+ if (!Number.isInteger(parsed) || parsed < 0) {
634
+ throw new Error("MSWARM_SELF_HOSTED_RESERVED_LLM_JOBS must be a non-negative integer");
635
+ }
636
+ return parsed;
637
+ }
638
+ function parseRelayPriority(value, fallback) {
639
+ const parsed = value === undefined || value === null || value === ""
640
+ ? fallback
641
+ : typeof value === "number"
642
+ ? value
643
+ : typeof value === "string"
644
+ ? Number(value)
645
+ : Number.NaN;
646
+ if (!Number.isInteger(parsed) || parsed < MIN_RELAY_PRIORITY || parsed > MAX_RELAY_PRIORITY) {
647
+ throw new Error(`MSWARM_SELF_HOSTED_RESERVED_PRIORITY_MAX must be an integer from ${MIN_RELAY_PRIORITY} to ${MAX_RELAY_PRIORITY}`);
648
+ }
649
+ return parsed;
650
+ }
651
+ function assertReservedLlmCapacity(input) {
652
+ const totalLlmJobs = input.maxConcurrentLlmJobs + input.reservedLlmJobs;
653
+ if (totalLlmJobs > input.maxConcurrentJobs) {
654
+ throw new Error("MSWARM_SELF_HOSTED_MAX_CONCURRENT_JOBS must be greater than or equal to " +
655
+ "MSWARM_SELF_HOSTED_MAX_CONCURRENT_LLM_JOBS plus MSWARM_SELF_HOSTED_RESERVED_LLM_JOBS");
656
+ }
657
+ }
590
658
  function parseBoolean(value, fallback) {
591
659
  if (typeof value === "boolean")
592
660
  return value;
@@ -1084,7 +1152,14 @@ function isHttpStatusError(error, status) {
1084
1152
  }
1085
1153
  const RELAY_RESULT_POST_RETRY_DELAYS_MS = [250, 1000, 2500];
1086
1154
  function runtimeErrorMessage(error) {
1087
- return error instanceof Error ? error.message : String(error);
1155
+ const message = error instanceof Error ? error.message : String(error);
1156
+ const cause = error instanceof Error
1157
+ ? error.cause
1158
+ : undefined;
1159
+ const causeCode = isRecord(cause) ? optionalText(cause.code) : null;
1160
+ return causeCode && !message.includes(causeCode)
1161
+ ? `${message} (${causeCode})`
1162
+ : message;
1088
1163
  }
1089
1164
  async function sleep(ms) {
1090
1165
  await new Promise((resolve) => setTimeout(resolve, ms));
@@ -1272,6 +1347,8 @@ function serviceEnvironment(config, env, homeDir) {
1272
1347
  MSWARM_SELF_HOSTED_JOB_TIMEOUT_MS: String(config.jobTimeoutMs),
1273
1348
  MSWARM_SELF_HOSTED_MAX_CONCURRENT_JOBS: String(config.maxConcurrentJobs || 1),
1274
1349
  MSWARM_SELF_HOSTED_MAX_CONCURRENT_LLM_JOBS: String(config.maxConcurrentLlmJobs || config.maxConcurrentJobs || 1),
1350
+ MSWARM_SELF_HOSTED_RESERVED_LLM_JOBS: String(config.reservedLlmJobs ?? 0),
1351
+ MSWARM_SELF_HOSTED_RESERVED_PRIORITY_MAX: String(config.reservedPriorityMax ?? DEFAULT_RESERVED_PRIORITY_MAX),
1275
1352
  MSWARM_SELF_HOSTED_GENERIC_JOBS_ENABLED: config.genericJobsEnabled ? "true" : "false",
1276
1353
  MSWARM_SELF_HOSTED_GENERIC_JOB_TIMEOUT_MS: String(config.genericJobTimeoutMs),
1277
1354
  MSWARM_SELF_HOSTED_GENERIC_JOB_MAX_CONCURRENCY: String(config.genericJobMaxConcurrency),
@@ -1687,6 +1764,9 @@ export async function readSelfHostedNodeConfig(env = process.env) {
1687
1764
  const packageNodeVersion = await readPackageNodeVersion();
1688
1765
  const maxConcurrentJobs = parsePositiveInteger(env.MSWARM_SELF_HOSTED_MAX_CONCURRENT_JOBS, state.max_concurrent_jobs || 1);
1689
1766
  const maxConcurrentLlmJobs = parsePositiveInteger(env.MSWARM_SELF_HOSTED_MAX_CONCURRENT_LLM_JOBS, state.max_concurrent_llm_jobs || maxConcurrentJobs);
1767
+ const reservedLlmJobs = parseNonNegativeInteger(env.MSWARM_SELF_HOSTED_RESERVED_LLM_JOBS, state.reserved_llm_jobs ?? 0);
1768
+ const reservedPriorityMax = parseRelayPriority(env.MSWARM_SELF_HOSTED_RESERVED_PRIORITY_MAX, state.reserved_priority_max ?? DEFAULT_RESERVED_PRIORITY_MAX);
1769
+ assertReservedLlmCapacity({ maxConcurrentJobs, maxConcurrentLlmJobs, reservedLlmJobs });
1690
1770
  return {
1691
1771
  gatewayBaseUrl: trimTrailingSlash(gatewayBaseUrl),
1692
1772
  jobsPollPath: optionalText(env.MSWARM_SELF_HOSTED_JOBS_POLL_PATH) ||
@@ -1730,6 +1810,8 @@ export async function readSelfHostedNodeConfig(env = process.env) {
1730
1810
  jobTimeoutMs: parsePositiveInteger(env.MSWARM_SELF_HOSTED_JOB_TIMEOUT_MS, state.job_timeout_ms || DEFAULT_JOB_TIMEOUT_MS),
1731
1811
  maxConcurrentJobs,
1732
1812
  maxConcurrentLlmJobs,
1813
+ reservedLlmJobs,
1814
+ reservedPriorityMax,
1733
1815
  genericJobsEnabled: parseBoolean(env.MSWARM_SELF_HOSTED_GENERIC_JOBS_ENABLED ?? env.MSWARM_SELF_HOSTED_GENERIC_JOBS, state.generic_jobs_enabled === true),
1734
1816
  genericJobTimeoutMs: parsePositiveInteger(env.MSWARM_SELF_HOSTED_GENERIC_JOB_TIMEOUT_MS, state.generic_job_timeout_ms || state.job_timeout_ms || DEFAULT_JOB_TIMEOUT_MS),
1735
1817
  genericJobMaxConcurrency: parsePositiveInteger(env.MSWARM_SELF_HOSTED_GENERIC_JOB_MAX_CONCURRENCY, state.generic_job_max_concurrency || 1),
@@ -1779,6 +1861,9 @@ export async function readOwnerSetupConfig(argv = process.argv.slice(3), env = p
1779
1861
  const packageNodeVersion = await readPackageNodeVersion();
1780
1862
  const maxConcurrentJobs = parsePositiveInteger(options["max-concurrent-jobs"] || env.MSWARM_SELF_HOSTED_MAX_CONCURRENT_JOBS, 1);
1781
1863
  const maxConcurrentLlmJobs = parsePositiveInteger(options["max-concurrent-llm-jobs"] || env.MSWARM_SELF_HOSTED_MAX_CONCURRENT_LLM_JOBS, maxConcurrentJobs);
1864
+ const reservedLlmJobs = parseNonNegativeInteger(options["reserved-llm-jobs"] ?? env.MSWARM_SELF_HOSTED_RESERVED_LLM_JOBS, 0);
1865
+ const reservedPriorityMax = parseRelayPriority(options["reserved-priority-max"] ?? env.MSWARM_SELF_HOSTED_RESERVED_PRIORITY_MAX, DEFAULT_RESERVED_PRIORITY_MAX);
1866
+ assertReservedLlmCapacity({ maxConcurrentJobs, maxConcurrentLlmJobs, reservedLlmJobs });
1782
1867
  return {
1783
1868
  apiKey,
1784
1869
  handle: handle || undefined,
@@ -1804,6 +1889,8 @@ export async function readOwnerSetupConfig(argv = process.argv.slice(3), env = p
1804
1889
  jobTimeoutMs: parsePositiveInteger(options["job-timeout-ms"] || env.MSWARM_SELF_HOSTED_JOB_TIMEOUT_MS, DEFAULT_JOB_TIMEOUT_MS),
1805
1890
  maxConcurrentJobs,
1806
1891
  maxConcurrentLlmJobs,
1892
+ reservedLlmJobs,
1893
+ reservedPriorityMax,
1807
1894
  genericJobsEnabled: parseBoolean(options["enable-generic-jobs"] || env.MSWARM_SELF_HOSTED_GENERIC_JOBS_ENABLED || env.MSWARM_SELF_HOSTED_GENERIC_JOBS, false),
1808
1895
  genericJobTimeoutMs: parsePositiveInteger(options["generic-job-timeout-ms"] || env.MSWARM_SELF_HOSTED_GENERIC_JOB_TIMEOUT_MS, DEFAULT_JOB_TIMEOUT_MS),
1809
1896
  genericJobMaxConcurrency: parsePositiveInteger(options["generic-job-max-concurrency"] || env.MSWARM_SELF_HOSTED_GENERIC_JOB_MAX_CONCURRENCY, 1),
@@ -4092,6 +4179,7 @@ export class MswarmSelfHostedNodeClient {
4092
4179
  this.jobsResultPathTemplate = lifecyclePath(input.jobsResultPathTemplate, DEFAULT_SELF_HOSTED_JOBS_RESULT_PATH_TEMPLATE);
4093
4180
  this.fetchImpl = input.fetchImpl || fetch;
4094
4181
  this.timeoutMs = input.timeoutMs || DEFAULT_REQUEST_TIMEOUT_MS;
4182
+ this.resultTimeoutMs = Math.max(this.timeoutMs, input.resultTimeoutMs || DEFAULT_RESULT_POST_TIMEOUT_MS);
4095
4183
  }
4096
4184
  lifecycleEndpoint(kind) {
4097
4185
  const path = kind === "poll"
@@ -4206,7 +4294,7 @@ export class MswarmSelfHostedNodeClient {
4206
4294
  authorization: `Bearer ${runtimeToken}`
4207
4295
  },
4208
4296
  body: JSON.stringify(payload)
4209
- }, this.timeoutMs);
4297
+ }, this.resultTimeoutMs);
4210
4298
  }
4211
4299
  async postJobStart(runtimeToken, jobId, payload) {
4212
4300
  return fetchJson(this.fetchImpl, this.lifecycleUrl("start", jobId), {
@@ -4238,8 +4326,15 @@ export class SelfHostedNodeRuntime {
4238
4326
  this.latencySamplesMs = [];
4239
4327
  this.recentFailures = [];
4240
4328
  this.lifecyclePollingDisabled = false;
4329
+ assertReservedLlmCapacity({
4330
+ maxConcurrentJobs: Math.max(1, Math.floor(config.maxConcurrentJobs || 1)),
4331
+ maxConcurrentLlmJobs: Math.max(1, Math.floor(config.maxConcurrentLlmJobs || config.maxConcurrentJobs || 1)),
4332
+ reservedLlmJobs: parseNonNegativeInteger(config.reservedLlmJobs, 0)
4333
+ });
4334
+ parseRelayPriority(config.reservedPriorityMax, DEFAULT_RESERVED_PRIORITY_MAX);
4241
4335
  this.config = config;
4242
4336
  this.fetchImpl = deps?.fetchImpl || fetch;
4337
+ this.useDedicatedGenerativeDispatcher = deps?.fetchImpl === undefined;
4243
4338
  this.gateway =
4244
4339
  deps?.gateway ||
4245
4340
  new MswarmSelfHostedNodeClient({
@@ -4249,7 +4344,8 @@ export class SelfHostedNodeRuntime {
4249
4344
  jobsEventsPathTemplate: config.jobsEventsPathTemplate,
4250
4345
  jobsResultPathTemplate: config.jobsResultPathTemplate,
4251
4346
  fetchImpl: this.fetchImpl,
4252
- timeoutMs: config.requestTimeoutMs
4347
+ timeoutMs: config.requestTimeoutMs,
4348
+ resultTimeoutMs: Math.min(config.jobTimeoutMs, DEFAULT_RESULT_POST_TIMEOUT_MS)
4253
4349
  });
4254
4350
  this.mcoda =
4255
4351
  deps?.mcoda ||
@@ -4439,7 +4535,8 @@ export class SelfHostedNodeRuntime {
4439
4535
  new MswarmSelfHostedNodeClient({
4440
4536
  gatewayBaseUrl: setupConfig.gatewayBaseUrl,
4441
4537
  fetchImpl: deps?.fetchImpl,
4442
- timeoutMs: setupConfig.requestTimeoutMs
4538
+ timeoutMs: setupConfig.requestTimeoutMs,
4539
+ resultTimeoutMs: Math.min(setupConfig.jobTimeoutMs, DEFAULT_RESULT_POST_TIMEOUT_MS)
4443
4540
  });
4444
4541
  const machineId = await readOrCreateSelfHostedMachineId(setupConfig.machineIdPath);
4445
4542
  const machineFingerprint = machineFingerprintFromId(machineId);
@@ -4508,6 +4605,8 @@ export class SelfHostedNodeRuntime {
4508
4605
  job_timeout_ms: setupConfig.jobTimeoutMs,
4509
4606
  max_concurrent_jobs: setupConfig.maxConcurrentJobs,
4510
4607
  max_concurrent_llm_jobs: setupConfig.maxConcurrentLlmJobs,
4608
+ reserved_llm_jobs: setupConfig.reservedLlmJobs,
4609
+ reserved_priority_max: setupConfig.reservedPriorityMax,
4511
4610
  generic_jobs_enabled: setupConfig.genericJobsEnabled,
4512
4611
  generic_job_timeout_ms: setupConfig.genericJobTimeoutMs,
4513
4612
  generic_job_max_concurrency: setupConfig.genericJobMaxConcurrency,
@@ -4532,7 +4631,8 @@ export class SelfHostedNodeRuntime {
4532
4631
  jobsEventsPathTemplate,
4533
4632
  jobsResultPathTemplate,
4534
4633
  fetchImpl: deps?.fetchImpl,
4535
- timeoutMs: setupConfig.requestTimeoutMs
4634
+ timeoutMs: setupConfig.requestTimeoutMs,
4635
+ resultTimeoutMs: Math.min(setupConfig.jobTimeoutMs, DEFAULT_RESULT_POST_TIMEOUT_MS)
4536
4636
  });
4537
4637
  const runtime = new SelfHostedNodeRuntime({
4538
4638
  gatewayBaseUrl: trimTrailingSlash(relayGatewayBaseUrl),
@@ -4563,6 +4663,8 @@ export class SelfHostedNodeRuntime {
4563
4663
  jobTimeoutMs: setupConfig.jobTimeoutMs,
4564
4664
  maxConcurrentJobs: setupConfig.maxConcurrentJobs,
4565
4665
  maxConcurrentLlmJobs: setupConfig.maxConcurrentLlmJobs,
4666
+ reservedLlmJobs: setupConfig.reservedLlmJobs,
4667
+ reservedPriorityMax: setupConfig.reservedPriorityMax,
4566
4668
  genericJobsEnabled: setupConfig.genericJobsEnabled,
4567
4669
  genericJobTimeoutMs: setupConfig.genericJobTimeoutMs,
4568
4670
  genericJobMaxConcurrency: setupConfig.genericJobMaxConcurrency,
@@ -4693,6 +4795,8 @@ export class SelfHostedNodeRuntime {
4693
4795
  job_timeout_ms: this.config.jobTimeoutMs,
4694
4796
  max_concurrent_jobs: this.config.maxConcurrentJobs,
4695
4797
  max_concurrent_llm_jobs: this.config.maxConcurrentLlmJobs,
4798
+ reserved_llm_jobs: this.config.reservedLlmJobs ?? 0,
4799
+ reserved_priority_max: this.config.reservedPriorityMax ?? DEFAULT_RESERVED_PRIORITY_MAX,
4696
4800
  generic_jobs_enabled: this.config.genericJobsEnabled,
4697
4801
  generic_job_timeout_ms: this.config.genericJobTimeoutMs,
4698
4802
  generic_job_max_concurrency: this.config.genericJobMaxConcurrency,
@@ -5016,14 +5120,20 @@ export class SelfHostedNodeRuntime {
5016
5120
  const jsonResponseByteLimit = generativeJsonResponseByteLimit(outputByteLimit);
5017
5121
  const controller = new AbortController();
5018
5122
  const timeout = setTimeout(() => controller.abort(), this.config.jobTimeoutMs);
5123
+ const dispatcher = this.useDedicatedGenerativeDispatcher
5124
+ ? new Agent({ headersTimeout: 0, bodyTimeout: 0 })
5125
+ : null;
5019
5126
  let payload;
5020
5127
  try {
5021
- const response = await this.fetchImpl(prepared.url, {
5128
+ const request = {
5022
5129
  method: "POST",
5023
5130
  headers: prepared.headers,
5024
5131
  body: JSON.stringify(prepared.body),
5025
5132
  signal: controller.signal
5026
- });
5133
+ };
5134
+ const response = dispatcher
5135
+ ? (await undiciFetch(prepared.url, { ...request, dispatcher }))
5136
+ : await this.fetchImpl(prepared.url, request);
5027
5137
  if (!response.ok) {
5028
5138
  await response.body?.cancel().catch(() => undefined);
5029
5139
  throw new Error(`local ${operation} runner request failed with status ${response.status}`);
@@ -5044,6 +5154,7 @@ export class SelfHostedNodeRuntime {
5044
5154
  }
5045
5155
  finally {
5046
5156
  clearTimeout(timeout);
5157
+ await dispatcher?.close().catch(() => undefined);
5047
5158
  }
5048
5159
  if (!isRecord(payload) || !Array.isArray(payload.data) || payload.data.length === 0) {
5049
5160
  throw new Error(`local ${operation} runner returned an invalid base64 response`);
@@ -5643,7 +5754,7 @@ export class SelfHostedNodeRuntime {
5643
5754
  return result;
5644
5755
  }
5645
5756
  catch (error) {
5646
- const message = redactRuntimeSecretValues(error instanceof Error ? error.message : String(error), [selectedAgent?.apiKey, options.attachedMswarmApiKey]);
5757
+ const message = redactRuntimeSecretValues(runtimeErrorMessage(error), [selectedAgent?.apiKey, options.attachedMswarmApiKey]);
5647
5758
  const explicitCode = selfHostedErrorCode(error);
5648
5759
  const code = explicitCode ??
5649
5760
  (/timeout/i.test(message)
@@ -5837,11 +5948,11 @@ export class SelfHostedNodeRuntime {
5837
5948
  });
5838
5949
  return { count: models.filter((model) => model.exposed !== false).length, response };
5839
5950
  }
5840
- async pollAndExecuteJob(waitMs = DEFAULT_JOB_POLL_WAIT_MS) {
5951
+ async pollRelayJob(waitMs = DEFAULT_JOB_POLL_WAIT_MS, options = {}) {
5841
5952
  const lifecycleState = await this.readLifecycleState();
5842
5953
  if (this.lifecyclePollingDisabled || this.isLifecycleProtocolDegradedState(lifecycleState)) {
5843
5954
  this.lifecyclePollingDisabled = true;
5844
- return { executed: false, status: "failed" };
5955
+ return { claim: null, status: "failed" };
5845
5956
  }
5846
5957
  const enrollment = await this.ensureEnrolled();
5847
5958
  const pollCapacity = this.buildLoadTelemetry({ models: [] });
@@ -5855,7 +5966,17 @@ export class SelfHostedNodeRuntime {
5855
5966
  max_jobs: pollCapacity.max_concurrency,
5856
5967
  max_concurrency: pollCapacity.max_concurrency,
5857
5968
  free_slots: pollCapacity.free_slots,
5858
- drain_mode: pollCapacity.drain_mode
5969
+ drain_mode: pollCapacity.drain_mode,
5970
+ ...(options.excludedAgentSlugs !== undefined
5971
+ ? {
5972
+ excluded_agent_slugs: boundedRelayAgentAliases([
5973
+ options.excludedAgentSlugs
5974
+ ])
5975
+ }
5976
+ : {}),
5977
+ ...(options.maxRequestedPriority !== undefined
5978
+ ? { max_requested_priority: options.maxRequestedPriority }
5979
+ : {})
5859
5980
  },
5860
5981
  wait_ms: waitMs
5861
5982
  });
@@ -5864,14 +5985,24 @@ export class SelfHostedNodeRuntime {
5864
5985
  const mismatch = this.lifecycleProtocolMismatch("poll", error);
5865
5986
  if (mismatch) {
5866
5987
  await this.markLifecycleProtocolDegraded(mismatch);
5867
- return { executed: false, status: "failed" };
5988
+ return { claim: null, status: "failed" };
5868
5989
  }
5869
5990
  throw error;
5870
5991
  }
5871
5992
  const job = response.job || null;
5872
5993
  if (!job) {
5873
- return { executed: false };
5994
+ return { claim: null };
5874
5995
  }
5996
+ return {
5997
+ claim: {
5998
+ runtimeToken: enrollment.runtimeToken,
5999
+ job,
6000
+ attachedMswarmApiKey: optionalText(response.attached_mswarm_api_key) || undefined
6001
+ }
6002
+ };
6003
+ }
6004
+ async executeRelayJobClaim(claim) {
6005
+ const { job, runtimeToken } = claim;
5875
6006
  const chatStreaming = (normalizeSelfHostedGenerativeOperationName(job.operation) ?? "chat.completions") ===
5876
6007
  "chat.completions" &&
5877
6008
  job.openai_request.stream === true;
@@ -5884,7 +6015,7 @@ export class SelfHostedNodeRuntime {
5884
6015
  }
5885
6016
  const stream_events = pendingStreamEvents.splice(0, pendingStreamEvents.length);
5886
6017
  try {
5887
- await this.gateway.postJobEvents(enrollment.runtimeToken, job.job_id, {
6018
+ await this.gateway.postJobEvents(runtimeToken, job.job_id, {
5888
6019
  node_id: this.config.nodeId,
5889
6020
  stream_events
5890
6021
  });
@@ -5901,10 +6032,10 @@ export class SelfHostedNodeRuntime {
5901
6032
  }
5902
6033
  };
5903
6034
  let result = await this.executeJob(job, {
5904
- attachedMswarmApiKey: optionalText(response.attached_mswarm_api_key) || undefined,
6035
+ attachedMswarmApiKey: claim.attachedMswarmApiKey,
5905
6036
  onStarted: async (event) => {
5906
6037
  try {
5907
- await this.gateway.postJobStart(enrollment.runtimeToken, job.job_id, {
6038
+ await this.gateway.postJobStart(runtimeToken, job.job_id, {
5908
6039
  node_id: this.config.nodeId,
5909
6040
  agent_slug: event.agent_slug || job.agent_slug,
5910
6041
  source_agent_slug: event.source_agent_slug || job.source_agent_slug || null,
@@ -5959,7 +6090,7 @@ export class SelfHostedNodeRuntime {
5959
6090
  let resultPostError = null;
5960
6091
  for (let attempt = 0; attempt <= RELAY_RESULT_POST_RETRY_DELAYS_MS.length; attempt += 1) {
5961
6092
  try {
5962
- await this.gateway.postJobResult(enrollment.runtimeToken, job.job_id, this.jobResultPayload(postedResult));
6093
+ await this.gateway.postJobResult(runtimeToken, job.job_id, this.jobResultPayload(postedResult));
5963
6094
  resultPostError = null;
5964
6095
  break;
5965
6096
  }
@@ -5987,6 +6118,13 @@ export class SelfHostedNodeRuntime {
5987
6118
  }
5988
6119
  return { executed: true, job_id: job.job_id, status: result.status };
5989
6120
  }
6121
+ async pollAndExecuteJob(waitMs = DEFAULT_JOB_POLL_WAIT_MS) {
6122
+ const polled = await this.pollRelayJob(waitMs);
6123
+ if (!polled.claim) {
6124
+ return { executed: false, ...(polled.status ? { status: polled.status } : {}) };
6125
+ }
6126
+ return this.executeRelayJobClaim(polled.claim);
6127
+ }
5990
6128
  async doctor() {
5991
6129
  const checks = [];
5992
6130
  checks.push({ name: "config", ok: Boolean(this.config.nodeId), message: this.config.nodeId || "missing node id" });
@@ -6025,19 +6163,33 @@ export class SelfHostedNodeRuntime {
6025
6163
  let stopped = false;
6026
6164
  let timer = null;
6027
6165
  let pollTimer = null;
6166
+ let pollTimerDueAt = 0;
6028
6167
  let polling = false;
6029
- let pollFailureStreak = 0;
6168
+ let relayPollFailureStreak = 0;
6169
+ let relayExecutionFailureStreak = 0;
6170
+ let activeRelayExecutions = 0;
6171
+ let activeRegularRelayExecutions = 0;
6172
+ let relayExecutionSequence = 0;
6173
+ const activeRelayAgentAliases = new Map();
6030
6174
  let revoked = false;
6175
+ const regularRelayExecutionLimit = Math.max(1, Math.floor(this.config.maxConcurrentLlmJobs || this.config.maxConcurrentJobs || 1));
6176
+ const reservedRelayExecutionLimit = parseNonNegativeInteger(this.config.reservedLlmJobs, 0);
6177
+ const reservedPriorityMax = parseRelayPriority(this.config.reservedPriorityMax, DEFAULT_RESERVED_PRIORITY_MAX);
6178
+ const relayExecutionLimit = regularRelayExecutionLimit + reservedRelayExecutionLimit;
6179
+ const clearPollTimer = () => {
6180
+ if (pollTimer) {
6181
+ clearTimeout(pollTimer);
6182
+ pollTimer = null;
6183
+ }
6184
+ pollTimerDueAt = 0;
6185
+ };
6031
6186
  const halt = () => {
6032
6187
  stopped = true;
6033
6188
  if (timer) {
6034
6189
  clearTimeout(timer);
6035
6190
  timer = null;
6036
6191
  }
6037
- if (pollTimer) {
6038
- clearTimeout(pollTimer);
6039
- pollTimer = null;
6040
- }
6192
+ clearPollTimer();
6041
6193
  };
6042
6194
  // 410 means the gateway has retired this node's credentials: it was removed there.
6043
6195
  // Retrying at full rate achieves nothing but a hot loop and a flooded journal, so
@@ -6051,10 +6203,7 @@ export class SelfHostedNodeRuntime {
6051
6203
  if (!isHttpStatusError(error, 410)) {
6052
6204
  return false;
6053
6205
  }
6054
- if (pollTimer) {
6055
- clearTimeout(pollTimer);
6056
- pollTimer = null;
6057
- }
6206
+ clearPollTimer();
6058
6207
  if (!revoked) {
6059
6208
  revoked = true;
6060
6209
  console.error(`[mswarm] this node was removed at the gateway (node ${this.config.nodeId}). ` +
@@ -6065,42 +6214,112 @@ export class SelfHostedNodeRuntime {
6065
6214
  return true;
6066
6215
  };
6067
6216
  const noteAlive = () => {
6068
- if (!revoked) {
6217
+ const recoveredFromRevocation = revoked;
6218
+ const relayPollingIsBackedOff = (relayPollFailureStreak > 0 || relayExecutionFailureStreak > 0) && !polling;
6219
+ if (!recoveredFromRevocation && !relayPollingIsBackedOff) {
6220
+ return;
6221
+ }
6222
+ if (recoveredFromRevocation) {
6223
+ revoked = false;
6224
+ console.error(`[mswarm] node ${this.config.nodeId} is accepted at the gateway again; resuming normal operation.`);
6225
+ }
6226
+ clearPollTimer();
6227
+ relayPollFailureStreak = 0;
6228
+ relayExecutionFailureStreak = 0;
6229
+ schedulePoll(0);
6230
+ };
6231
+ const currentRelayRetryDelayMs = () => Math.max(pollRetryDelayMs(relayPollFailureStreak), pollRetryDelayMs(relayExecutionFailureStreak));
6232
+ const schedulePoll = (delayMs) => {
6233
+ if (stopped ||
6234
+ polling ||
6235
+ revoked ||
6236
+ this.lifecyclePollingDisabled ||
6237
+ this.config.relayMode === "direct" ||
6238
+ activeRelayExecutions >= relayExecutionLimit) {
6239
+ return;
6240
+ }
6241
+ const dueAt = Date.now() + Math.max(0, delayMs);
6242
+ if (pollTimer && pollTimerDueAt >= dueAt) {
6069
6243
  return;
6070
6244
  }
6071
- revoked = false;
6072
- console.error(`[mswarm] node ${this.config.nodeId} is accepted at the gateway again; resuming normal operation.`);
6073
- poll();
6245
+ clearPollTimer();
6246
+ pollTimerDueAt = dueAt;
6247
+ pollTimer = setTimeout(() => {
6248
+ pollTimer = null;
6249
+ pollTimerDueAt = 0;
6250
+ poll();
6251
+ }, Math.max(0, dueAt - Date.now()));
6074
6252
  };
6075
6253
  const poll = () => {
6076
6254
  if (stopped ||
6077
6255
  polling ||
6078
6256
  revoked ||
6079
6257
  this.lifecyclePollingDisabled ||
6080
- this.config.relayMode === "direct") {
6258
+ this.config.relayMode === "direct" ||
6259
+ activeRelayExecutions >= relayExecutionLimit) {
6081
6260
  return;
6082
6261
  }
6083
6262
  polling = true;
6084
- void this.pollAndExecuteJob()
6085
- .then(() => {
6086
- pollFailureStreak = 0;
6263
+ const reservedOnly = reservedRelayExecutionLimit > 0 &&
6264
+ activeRegularRelayExecutions >= regularRelayExecutionLimit &&
6265
+ activeRelayExecutions < relayExecutionLimit;
6266
+ const excludedAgentSlugs = boundedRelayAgentAliases(activeRelayAgentAliases.values());
6267
+ void this.pollRelayJob(reservedOnly ? RESERVED_JOB_POLL_WAIT_MS : DEFAULT_JOB_POLL_WAIT_MS, {
6268
+ ...(reservedRelayExecutionLimit > 0 ? { excludedAgentSlugs } : {}),
6269
+ ...(reservedOnly ? { maxRequestedPriority: reservedPriorityMax } : {})
6270
+ })
6271
+ .then((polled) => {
6272
+ relayPollFailureStreak = 0;
6273
+ const claim = polled.claim;
6274
+ if (!claim) {
6275
+ return;
6276
+ }
6277
+ const regularLane = !reservedOnly;
6278
+ const executionId = ++relayExecutionSequence;
6279
+ activeRelayExecutions += 1;
6280
+ if (regularLane) {
6281
+ activeRegularRelayExecutions += 1;
6282
+ }
6283
+ activeRelayAgentAliases.set(executionId, relayJobAgentAliases(claim.job));
6284
+ void this.executeRelayJobClaim(claim)
6285
+ .then(() => {
6286
+ relayExecutionFailureStreak = 0;
6287
+ })
6288
+ .catch((error) => {
6289
+ if (noteRevoked(error)) {
6290
+ return;
6291
+ }
6292
+ relayExecutionFailureStreak += 1;
6293
+ console.error(`[mswarm] self-hosted relay job ${claim.job.job_id} failed: ${runtimeErrorMessage(error)}`);
6294
+ })
6295
+ .finally(() => {
6296
+ activeRelayExecutions = Math.max(0, activeRelayExecutions - 1);
6297
+ if (regularLane) {
6298
+ activeRegularRelayExecutions = Math.max(0, activeRegularRelayExecutions - 1);
6299
+ }
6300
+ activeRelayAgentAliases.delete(executionId);
6301
+ schedulePoll(currentRelayRetryDelayMs());
6302
+ });
6087
6303
  })
6088
6304
  .catch((error) => {
6089
6305
  if (noteRevoked(error)) {
6090
6306
  return;
6091
6307
  }
6092
- pollFailureStreak += 1;
6308
+ relayPollFailureStreak += 1;
6093
6309
  console.error(`[mswarm] self-hosted relay poll failed: ${runtimeErrorMessage(error)}`);
6094
6310
  })
6095
6311
  .finally(() => {
6096
6312
  polling = false;
6097
- if (stopped || revoked) {
6313
+ if (stopped ||
6314
+ revoked ||
6315
+ this.lifecyclePollingDisabled ||
6316
+ activeRelayExecutions >= relayExecutionLimit) {
6098
6317
  return;
6099
6318
  }
6100
6319
  // A healthy poll long-polls at the gateway, so re-entering immediately is
6101
6320
  // correct. A failing one returns at once, so without this the loop spins as
6102
6321
  // fast as the network allows.
6103
- pollTimer = setTimeout(poll, pollRetryDelayMs(pollFailureStreak));
6322
+ schedulePoll(currentRelayRetryDelayMs());
6104
6323
  });
6105
6324
  };
6106
6325
  const schedule = () => {
@@ -6122,7 +6341,7 @@ export class SelfHostedNodeRuntime {
6122
6341
  })
6123
6342
  .finally(() => {
6124
6343
  schedule();
6125
- poll();
6344
+ schedulePoll(0);
6126
6345
  });
6127
6346
  return { stop: halt };
6128
6347
  }