@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/README.md +17 -0
- package/dist/runtime.d.ts +17 -5
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +257 -38
- package/dist/runtime.js.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +4 -0
- package/dist/server.js.map +1 -1
- package/package.json +4 -3
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
|
-
|
|
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.
|
|
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
|
|
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(
|
|
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
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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(
|
|
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:
|
|
6035
|
+
attachedMswarmApiKey: claim.attachedMswarmApiKey,
|
|
5905
6036
|
onStarted: async (event) => {
|
|
5906
6037
|
try {
|
|
5907
|
-
await this.gateway.postJobStart(
|
|
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(
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
6072
|
-
|
|
6073
|
-
|
|
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
|
-
|
|
6085
|
-
|
|
6086
|
-
|
|
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
|
-
|
|
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 ||
|
|
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
|
-
|
|
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
|
-
|
|
6344
|
+
schedulePoll(0);
|
|
6126
6345
|
});
|
|
6127
6346
|
return { stop: halt };
|
|
6128
6347
|
}
|