@mcoda/mswarm 0.1.0 → 0.1.45

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
@@ -1,16 +1,23 @@
1
- import { mkdir, readFile, writeFile } from "node:fs/promises";
1
+ import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
2
2
  import { dirname, join } from "node:path";
3
- import { homedir } from "node:os";
3
+ import { hostname, homedir, platform, userInfo } from "node:os";
4
4
  import { spawn } from "node:child_process";
5
+ import { createHash, randomUUID } from "node:crypto";
5
6
  const DEFAULT_GATEWAY_BASE_URL = "http://127.0.0.1:8080";
7
+ const DEFAULT_SETUP_GATEWAY_BASE_URL = "https://api.mswarm.org";
6
8
  const DEFAULT_OLLAMA_BASE_URL = "http://127.0.0.1:11434";
7
9
  const DEFAULT_LISTEN_HOST = "127.0.0.1";
8
10
  const DEFAULT_LISTEN_PORT = 18083;
9
11
  const DEFAULT_HEARTBEAT_INTERVAL_SECONDS = 30;
10
- const DEFAULT_REQUEST_TIMEOUT_MS = 10_000;
12
+ const DEFAULT_REQUEST_TIMEOUT_MS = 10000;
11
13
  const DEFAULT_MCODA_BIN = "mcoda";
12
14
  const DEFAULT_MCODA_LIST_ARGS = ["agent", "list", "--json", "--refresh-health"];
13
15
  const DEFAULT_COMMAND_MAX_BUFFER = 16 * 1024 * 1024;
16
+ const DEFAULT_JOB_POLL_WAIT_MS = 25000;
17
+ const SERVICE_LABEL = "com.mcoda.mswarm.self-hosted-node";
18
+ const SYSTEMD_SERVICE_NAME = "mswarm-self-hosted-node.service";
19
+ const WINDOWS_TASK_NAME = "MswarmSelfHostedNode";
20
+ const WINDOWS_WRAPPER_SCRIPT_NAME = "mswarm-self-hosted-node.ps1";
14
21
  function optionalText(value) {
15
22
  return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
16
23
  }
@@ -53,6 +60,48 @@ function parseDiscoveryMode(value) {
53
60
  const normalized = typeof value === "string" ? value.trim().toLowerCase() : "";
54
61
  return normalized === "ollama" ? "ollama" : "mcoda";
55
62
  }
63
+ function parseRelayMode(value) {
64
+ const normalized = typeof value === "string" ? value.trim().toLowerCase() : "";
65
+ return normalized === "direct" ? "direct" : "outbound";
66
+ }
67
+ function normalizeLocalName(value) {
68
+ return (value
69
+ .trim()
70
+ .toLowerCase()
71
+ .replace(/[^a-z0-9]+/g, "-")
72
+ .replace(/^-+|-+$/g, "")
73
+ .slice(0, 80) || "local-node");
74
+ }
75
+ export function resolveDefaultServerName() {
76
+ return normalizeLocalName(hostname() || "local-node");
77
+ }
78
+ function defaultMachineIdPath() {
79
+ return join(homedir(), ".mswarm", "self-hosted-node", "machine.id");
80
+ }
81
+ function parseCliOptions(argv) {
82
+ const options = {};
83
+ for (let index = 0; index < argv.length; index += 1) {
84
+ const token = argv[index];
85
+ if (!token.startsWith("--")) {
86
+ continue;
87
+ }
88
+ const equalsIndex = token.indexOf("=");
89
+ if (equalsIndex > 0) {
90
+ options[token.slice(2, equalsIndex)] = token.slice(equalsIndex + 1);
91
+ continue;
92
+ }
93
+ const key = token.slice(2);
94
+ const next = argv[index + 1];
95
+ if (next && !next.startsWith("--")) {
96
+ options[key] = next;
97
+ index += 1;
98
+ }
99
+ else {
100
+ options[key] = true;
101
+ }
102
+ }
103
+ return options;
104
+ }
56
105
  function trimTrailingSlash(value) {
57
106
  return value.replace(/\/+$/g, "");
58
107
  }
@@ -62,6 +111,26 @@ function defaultStatePath() {
62
111
  function defaultRuntimeTokenPath() {
63
112
  return join(homedir(), ".mswarm", "self-hosted-node", "node.key");
64
113
  }
114
+ export async function readOrCreateSelfHostedMachineId(machineIdPath = defaultMachineIdPath()) {
115
+ try {
116
+ const existing = (await readFile(machineIdPath, "utf8")).trim();
117
+ if (existing) {
118
+ return existing;
119
+ }
120
+ }
121
+ catch (error) {
122
+ if (error.code !== "ENOENT") {
123
+ throw error;
124
+ }
125
+ }
126
+ const machineId = randomUUID();
127
+ await mkdir(dirname(machineIdPath), { recursive: true });
128
+ await writeFile(machineIdPath, `${machineId}\n`, { encoding: "utf8", mode: 0o600 });
129
+ return machineId;
130
+ }
131
+ export function machineFingerprintFromId(machineId) {
132
+ return `sha256:${createHash("sha256").update(machineId.trim()).digest("hex")}`;
133
+ }
65
134
  function isVisionModel(modelName, family) {
66
135
  const normalized = `${modelName} ${family || ""}`.toLowerCase();
67
136
  return normalized.includes("llava") || normalized.includes("vision") || normalized.includes("bakllava");
@@ -198,6 +267,374 @@ export async function writeSelfHostedRuntimeToken(tokenPath, runtimeToken) {
198
267
  await mkdir(dirname(tokenPath), { recursive: true });
199
268
  await writeFile(tokenPath, `${runtimeToken.trim()}\n`, { encoding: "utf8", mode: 0o600 });
200
269
  }
270
+ function escapeXml(value) {
271
+ return value
272
+ .replace(/&/g, "&amp;")
273
+ .replace(/</g, "&lt;")
274
+ .replace(/>/g, "&gt;")
275
+ .replace(/"/g, "&quot;")
276
+ .replace(/'/g, "&apos;");
277
+ }
278
+ function quoteSystemdValue(value) {
279
+ return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
280
+ }
281
+ function serviceLogDir(homeDir) {
282
+ return join(homeDir, ".mswarm", "self-hosted-node");
283
+ }
284
+ function serviceEnvironment(config, env, homeDir) {
285
+ const values = {
286
+ HOME: env.HOME || homeDir,
287
+ PATH: env.PATH || env.Path || env.path || "/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin",
288
+ MSWARM_GATEWAY_BASE_URL: config.gatewayBaseUrl,
289
+ MSWARM_SELF_HOSTED_NODE_STATE_PATH: config.statePath,
290
+ MSWARM_SELF_HOSTED_NODE_KEY_PATH: config.runtimeTokenPath,
291
+ MSWARM_SELF_HOSTED_RELAY_MODE: config.relayMode || "outbound",
292
+ MSWARM_SELF_HOSTED_DIRECT_BASE_URL: config.directBaseUrl || null,
293
+ MSWARM_SELF_HOSTED_DISCOVERY_MODE: config.discoveryMode,
294
+ MSWARM_SELF_HOSTED_MCODA_BIN: config.mcodaBin,
295
+ MSWARM_SELF_HOSTED_MCODA_LIST_ARGS: config.mcodaListArgs.join(","),
296
+ MSWARM_SELF_HOSTED_OLLAMA_BASE_URL: config.ollamaBaseUrl,
297
+ MSWARM_SELF_HOSTED_NODE_VERSION: config.nodeVersion,
298
+ MSWARM_SELF_HOSTED_EXPOSE_ALL_MODELS: config.exposeAllModels ? "true" : "false",
299
+ MSWARM_SELF_HOSTED_MODEL_ALLOWLIST: config.modelAllowlist.join(","),
300
+ MSWARM_SELF_HOSTED_MODEL_BLOCKLIST: config.modelBlocklist.join(","),
301
+ MSWARM_SELF_HOSTED_HEARTBEAT_INTERVAL_SECONDS: String(config.heartbeatIntervalSeconds),
302
+ MSWARM_SELF_HOSTED_REQUEST_TIMEOUT_MS: String(config.requestTimeoutMs)
303
+ };
304
+ return Object.fromEntries(Object.entries(values).filter((entry) => typeof entry[1] === "string" && entry[1] !== ""));
305
+ }
306
+ function buildLaunchdPlist(input) {
307
+ const env = Object.entries(input.env)
308
+ .map(([key, value]) => ` <key>${escapeXml(key)}</key>\n <string>${escapeXml(value)}</string>`)
309
+ .join("\n");
310
+ return `<?xml version="1.0" encoding="UTF-8"?>
311
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
312
+ <plist version="1.0">
313
+ <dict>
314
+ <key>Label</key>
315
+ <string>${escapeXml(input.label)}</string>
316
+ <key>ProgramArguments</key>
317
+ <array>
318
+ <string>${escapeXml(input.nodePath)}</string>
319
+ <string>${escapeXml(input.commandPath)}</string>
320
+ <string>start</string>
321
+ </array>
322
+ <key>EnvironmentVariables</key>
323
+ <dict>
324
+ ${env}
325
+ </dict>
326
+ <key>RunAtLoad</key>
327
+ <true/>
328
+ <key>KeepAlive</key>
329
+ <true/>
330
+ <key>StandardOutPath</key>
331
+ <string>${escapeXml(input.logPath)}</string>
332
+ <key>StandardErrorPath</key>
333
+ <string>${escapeXml(input.errorLogPath)}</string>
334
+ </dict>
335
+ </plist>
336
+ `;
337
+ }
338
+ function buildSystemdUserService(input) {
339
+ const env = Object.entries(input.env)
340
+ .map(([key, value]) => `Environment=${quoteSystemdValue(`${key}=${value}`)}`)
341
+ .join("\n");
342
+ return `[Unit]
343
+ Description=mswarm self-hosted node
344
+ After=network-online.target
345
+ Wants=network-online.target
346
+
347
+ [Service]
348
+ Type=simple
349
+ ExecStart=${quoteSystemdValue(input.nodePath)} ${quoteSystemdValue(input.commandPath)} start
350
+ Restart=always
351
+ RestartSec=5
352
+ StandardOutput=append:${input.logPath}
353
+ StandardError=append:${input.errorLogPath}
354
+ ${env}
355
+
356
+ [Install]
357
+ WantedBy=default.target
358
+ `;
359
+ }
360
+ function quotePowerShellValue(value) {
361
+ return `'${value.replace(/'/g, "''")}'`;
362
+ }
363
+ function quoteWindowsCommandArg(value) {
364
+ return `"${value.replace(/"/g, '\\"')}"`;
365
+ }
366
+ function buildWindowsTaskWrapperScript(input) {
367
+ const env = Object.entries(input.env)
368
+ .map(([key, value]) => `$env:${key} = ${quotePowerShellValue(value)}`)
369
+ .join("\n");
370
+ return `$ErrorActionPreference = 'Continue'
371
+
372
+ $logPath = ${quotePowerShellValue(input.logPath)}
373
+ $errorLogPath = ${quotePowerShellValue(input.errorLogPath)}
374
+ $nodePath = ${quotePowerShellValue(input.nodePath)}
375
+ $commandArguments = @(${quotePowerShellValue(input.commandPath)}, 'start')
376
+
377
+ New-Item -ItemType Directory -Force -Path (Split-Path -Parent $logPath) | Out-Null
378
+
379
+ ${env}
380
+
381
+ while ($true) {
382
+ $startedAt = Get-Date -Format o
383
+ Add-Content -Path $logPath -Value "[$startedAt] starting mswarm self-hosted node"
384
+ try {
385
+ & $nodePath @commandArguments >> $logPath 2>> $errorLogPath
386
+ $exitCode = if ($null -eq $LASTEXITCODE) { 0 } else { $LASTEXITCODE }
387
+ $endedAt = Get-Date -Format o
388
+ Add-Content -Path $errorLogPath -Value "[$endedAt] mswarm self-hosted node exited with code $exitCode; restarting in 5 seconds"
389
+ } catch {
390
+ $failedAt = Get-Date -Format o
391
+ Add-Content -Path $errorLogPath -Value "[$failedAt] mswarm self-hosted node wrapper error: $_"
392
+ }
393
+ Start-Sleep -Seconds 5
394
+ }
395
+ `;
396
+ }
397
+ function buildWindowsTaskRegistrationCommand(wrapperPath) {
398
+ const actionArguments = `-NoProfile -ExecutionPolicy Bypass -File ${quoteWindowsCommandArg(wrapperPath)}`;
399
+ const settings = "New-ScheduledTaskSettingsSet " +
400
+ "-AllowStartIfOnBatteries " +
401
+ "-DontStopIfGoingOnBatteries " +
402
+ "-StartWhenAvailable " +
403
+ "-MultipleInstances IgnoreNew " +
404
+ "-RestartCount 999 " +
405
+ "-RestartInterval (New-TimeSpan -Minutes 1) " +
406
+ "-ExecutionTimeLimit (New-TimeSpan -Seconds 0)";
407
+ return [
408
+ `Stop-ScheduledTask -TaskName ${quotePowerShellValue(WINDOWS_TASK_NAME)} -ErrorAction SilentlyContinue`,
409
+ `$action = New-ScheduledTaskAction -Execute 'powershell.exe' -Argument ${quotePowerShellValue(actionArguments)}`,
410
+ "$trigger = New-ScheduledTaskTrigger -AtLogOn",
411
+ `$settings = ${settings}`,
412
+ `Register-ScheduledTask -TaskName ${quotePowerShellValue(WINDOWS_TASK_NAME)} -Action $action -Trigger $trigger -Settings $settings -Description 'mswarm self-hosted node daemon' -Force | Out-Null`
413
+ ].join("; ");
414
+ }
415
+ function windowsTaskCommand(command) {
416
+ return `${command} -TaskName ${quotePowerShellValue(WINDOWS_TASK_NAME)}`;
417
+ }
418
+ function launchdDomain() {
419
+ const uid = typeof process.getuid === "function" ? process.getuid() : userInfo().uid;
420
+ return `gui/${uid}`;
421
+ }
422
+ export function resolveSelfHostedNodeServiceLayout(input = {}) {
423
+ const targetPlatform = input.platform || platform();
424
+ const homeDir = input.homeDir || homedir();
425
+ const logDir = serviceLogDir(homeDir);
426
+ const logPath = join(logDir, "daemon.log");
427
+ const errorLogPath = join(logDir, "daemon.err.log");
428
+ if (targetPlatform === "darwin") {
429
+ return {
430
+ platform: targetPlatform,
431
+ manager: "launchd",
432
+ serviceName: SERVICE_LABEL,
433
+ servicePath: join(homeDir, "Library", "LaunchAgents", `${SERVICE_LABEL}.plist`),
434
+ logPath,
435
+ errorLogPath
436
+ };
437
+ }
438
+ if (targetPlatform === "linux") {
439
+ return {
440
+ platform: targetPlatform,
441
+ manager: "systemd",
442
+ serviceName: SYSTEMD_SERVICE_NAME,
443
+ servicePath: join(homeDir, ".config", "systemd", "user", SYSTEMD_SERVICE_NAME),
444
+ logPath,
445
+ errorLogPath
446
+ };
447
+ }
448
+ if (targetPlatform === "win32") {
449
+ return {
450
+ platform: targetPlatform,
451
+ manager: "windows-task-scheduler",
452
+ serviceName: WINDOWS_TASK_NAME,
453
+ servicePath: join(logDir, WINDOWS_WRAPPER_SCRIPT_NAME),
454
+ logPath,
455
+ errorLogPath
456
+ };
457
+ }
458
+ throw new Error(`Persistent service control is not supported on ${targetPlatform}`);
459
+ }
460
+ function serviceControlResult(layout, action, result, ok = true, message) {
461
+ return {
462
+ manager: layout.manager,
463
+ serviceName: layout.serviceName,
464
+ servicePath: layout.servicePath,
465
+ logPath: layout.logPath,
466
+ errorLogPath: layout.errorLogPath,
467
+ action,
468
+ ok,
469
+ stdout: result.stdout,
470
+ stderr: result.stderr,
471
+ ...(message ? { message } : {})
472
+ };
473
+ }
474
+ async function runServiceCommand(runner, command, args, timeoutMs) {
475
+ return runner(command, args, { timeoutMs, maxBuffer: DEFAULT_COMMAND_MAX_BUFFER });
476
+ }
477
+ export async function installSelfHostedNodeService(config, options) {
478
+ const homeDir = options.homeDir || homedir();
479
+ const layout = resolveSelfHostedNodeServiceLayout({ platform: options.platform, homeDir });
480
+ const logDir = serviceLogDir(homeDir);
481
+ const env = serviceEnvironment(config, options.env || process.env, homeDir);
482
+ const nodePath = options.nodePath || process.execPath;
483
+ const runner = options.runner || defaultCommandRunner;
484
+ await mkdir(logDir, { recursive: true });
485
+ if (layout.platform === "darwin") {
486
+ await mkdir(dirname(layout.servicePath), { recursive: true });
487
+ await writeFile(layout.servicePath, buildLaunchdPlist({
488
+ label: SERVICE_LABEL,
489
+ nodePath,
490
+ commandPath: options.commandPath,
491
+ logPath: layout.logPath,
492
+ errorLogPath: layout.errorLogPath,
493
+ env
494
+ }), "utf8");
495
+ const domain = launchdDomain();
496
+ await runner("launchctl", ["bootout", `${domain}/${SERVICE_LABEL}`], {
497
+ timeoutMs: config.requestTimeoutMs,
498
+ maxBuffer: DEFAULT_COMMAND_MAX_BUFFER
499
+ }).catch(() => undefined);
500
+ await runner("launchctl", ["bootstrap", domain, layout.servicePath], {
501
+ timeoutMs: config.requestTimeoutMs,
502
+ maxBuffer: DEFAULT_COMMAND_MAX_BUFFER
503
+ });
504
+ await runner("launchctl", ["enable", `${domain}/${SERVICE_LABEL}`], {
505
+ timeoutMs: config.requestTimeoutMs,
506
+ maxBuffer: DEFAULT_COMMAND_MAX_BUFFER
507
+ }).catch(() => undefined);
508
+ await runner("launchctl", ["kickstart", "-k", `${domain}/${SERVICE_LABEL}`], {
509
+ timeoutMs: config.requestTimeoutMs,
510
+ maxBuffer: DEFAULT_COMMAND_MAX_BUFFER
511
+ });
512
+ return { ...layout, started: true };
513
+ }
514
+ if (layout.platform === "linux") {
515
+ await mkdir(dirname(layout.servicePath), { recursive: true });
516
+ await writeFile(layout.servicePath, buildSystemdUserService({
517
+ nodePath,
518
+ commandPath: options.commandPath,
519
+ logPath: layout.logPath,
520
+ errorLogPath: layout.errorLogPath,
521
+ env
522
+ }), "utf8");
523
+ await runner("systemctl", ["--user", "daemon-reload"], {
524
+ timeoutMs: config.requestTimeoutMs,
525
+ maxBuffer: DEFAULT_COMMAND_MAX_BUFFER
526
+ });
527
+ await runner("systemctl", ["--user", "enable", SYSTEMD_SERVICE_NAME], {
528
+ timeoutMs: config.requestTimeoutMs,
529
+ maxBuffer: DEFAULT_COMMAND_MAX_BUFFER
530
+ });
531
+ await runner("loginctl", ["enable-linger", options.env?.USER || options.env?.USERNAME || userInfo().username], {
532
+ timeoutMs: config.requestTimeoutMs,
533
+ maxBuffer: DEFAULT_COMMAND_MAX_BUFFER
534
+ }).catch(() => undefined);
535
+ await runner("systemctl", ["--user", "restart", SYSTEMD_SERVICE_NAME], {
536
+ timeoutMs: config.requestTimeoutMs,
537
+ maxBuffer: DEFAULT_COMMAND_MAX_BUFFER
538
+ });
539
+ return { ...layout, started: true };
540
+ }
541
+ if (layout.platform === "win32") {
542
+ await writeFile(layout.servicePath, buildWindowsTaskWrapperScript({
543
+ nodePath,
544
+ commandPath: options.commandPath,
545
+ logPath: layout.logPath,
546
+ errorLogPath: layout.errorLogPath,
547
+ env
548
+ }), "utf8");
549
+ await runner("powershell.exe", ["-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", buildWindowsTaskRegistrationCommand(layout.servicePath)], {
550
+ timeoutMs: config.requestTimeoutMs,
551
+ maxBuffer: DEFAULT_COMMAND_MAX_BUFFER
552
+ });
553
+ await runner("powershell.exe", ["-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", windowsTaskCommand("Start-ScheduledTask")], {
554
+ timeoutMs: config.requestTimeoutMs,
555
+ maxBuffer: DEFAULT_COMMAND_MAX_BUFFER
556
+ });
557
+ return { ...layout, started: true };
558
+ }
559
+ throw new Error(`Persistent service install is not supported on ${layout.platform}`);
560
+ }
561
+ export async function controlSelfHostedNodeService(action, options = {}) {
562
+ const layout = resolveSelfHostedNodeServiceLayout(options);
563
+ const runner = options.runner || defaultCommandRunner;
564
+ const timeoutMs = options.requestTimeoutMs || DEFAULT_REQUEST_TIMEOUT_MS;
565
+ try {
566
+ if (layout.manager === "launchd") {
567
+ const domain = launchdDomain();
568
+ const serviceTarget = `${domain}/${SERVICE_LABEL}`;
569
+ if (action === "stop") {
570
+ const result = await runServiceCommand(runner, "launchctl", ["bootout", serviceTarget], timeoutMs);
571
+ return serviceControlResult(layout, action, result);
572
+ }
573
+ if (action === "status") {
574
+ const result = await runServiceCommand(runner, "launchctl", ["print", serviceTarget], timeoutMs);
575
+ return serviceControlResult(layout, action, result);
576
+ }
577
+ if (action === "restart") {
578
+ await runServiceCommand(runner, "launchctl", ["bootout", serviceTarget], timeoutMs).catch(() => undefined);
579
+ }
580
+ await runServiceCommand(runner, "launchctl", ["bootstrap", domain, layout.servicePath], timeoutMs).catch(() => undefined);
581
+ await runServiceCommand(runner, "launchctl", ["enable", serviceTarget], timeoutMs).catch(() => undefined);
582
+ const result = await runServiceCommand(runner, "launchctl", ["kickstart", "-k", serviceTarget], timeoutMs);
583
+ return serviceControlResult(layout, action, result);
584
+ }
585
+ if (layout.manager === "systemd") {
586
+ const systemdAction = action === "status" ? "status" : action;
587
+ const args = action === "status"
588
+ ? ["--user", "status", "--no-pager", SYSTEMD_SERVICE_NAME]
589
+ : ["--user", systemdAction, SYSTEMD_SERVICE_NAME];
590
+ const result = await runServiceCommand(runner, "systemctl", args, timeoutMs);
591
+ return serviceControlResult(layout, action, result);
592
+ }
593
+ if (layout.manager === "windows-task-scheduler") {
594
+ const command = action === "status"
595
+ ? [
596
+ `$task = Get-ScheduledTask -TaskName ${quotePowerShellValue(WINDOWS_TASK_NAME)} -ErrorAction Stop`,
597
+ `$info = Get-ScheduledTaskInfo -TaskName ${quotePowerShellValue(WINDOWS_TASK_NAME)} -ErrorAction Stop`,
598
+ "[pscustomobject]@{TaskName=$task.TaskName;State=$task.State;LastRunTime=$info.LastRunTime;LastTaskResult=$info.LastTaskResult} | ConvertTo-Json -Compress"
599
+ ].join("; ")
600
+ : action === "restart"
601
+ ? `${windowsTaskCommand("Stop-ScheduledTask")} -ErrorAction SilentlyContinue; ${windowsTaskCommand("Start-ScheduledTask")}`
602
+ : windowsTaskCommand(action === "start" ? "Start-ScheduledTask" : "Stop-ScheduledTask");
603
+ const result = await runServiceCommand(runner, "powershell.exe", ["-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", command], timeoutMs);
604
+ return serviceControlResult(layout, action, result);
605
+ }
606
+ }
607
+ catch (error) {
608
+ if (action === "status") {
609
+ return serviceControlResult(layout, action, { stdout: "", stderr: "" }, false, error instanceof Error ? error.message : String(error));
610
+ }
611
+ throw error;
612
+ }
613
+ throw new Error(`Persistent service control is not supported on ${layout.platform}`);
614
+ }
615
+ export async function uninstallSelfHostedNodeService(options = {}) {
616
+ const layout = resolveSelfHostedNodeServiceLayout(options);
617
+ const runner = options.runner || defaultCommandRunner;
618
+ const timeoutMs = options.requestTimeoutMs || DEFAULT_REQUEST_TIMEOUT_MS;
619
+ if (layout.manager === "launchd") {
620
+ await runServiceCommand(runner, "launchctl", ["bootout", `${launchdDomain()}/${SERVICE_LABEL}`], timeoutMs).catch(() => undefined);
621
+ await rm(layout.servicePath, { force: true });
622
+ return serviceControlResult(layout, "uninstall", { stdout: "", stderr: "" });
623
+ }
624
+ if (layout.manager === "systemd") {
625
+ await runServiceCommand(runner, "systemctl", ["--user", "disable", "--now", SYSTEMD_SERVICE_NAME], timeoutMs).catch(() => undefined);
626
+ await rm(layout.servicePath, { force: true });
627
+ await runServiceCommand(runner, "systemctl", ["--user", "daemon-reload"], timeoutMs).catch(() => undefined);
628
+ return serviceControlResult(layout, "uninstall", { stdout: "", stderr: "" });
629
+ }
630
+ if (layout.manager === "windows-task-scheduler") {
631
+ const command = `${windowsTaskCommand("Stop-ScheduledTask")} -ErrorAction SilentlyContinue; Unregister-ScheduledTask -TaskName ${quotePowerShellValue(WINDOWS_TASK_NAME)} -Confirm:$false -ErrorAction SilentlyContinue`;
632
+ await runServiceCommand(runner, "powershell.exe", ["-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", command], timeoutMs).catch(() => undefined);
633
+ await rm(layout.servicePath, { force: true });
634
+ return serviceControlResult(layout, "uninstall", { stdout: "", stderr: "" });
635
+ }
636
+ throw new Error(`Persistent service uninstall is not supported on ${layout.platform}`);
637
+ }
201
638
  export async function readSelfHostedNodeConfig(env = process.env) {
202
639
  const statePath = optionalText(env.MSWARM_SELF_HOSTED_NODE_STATE_PATH) || defaultStatePath();
203
640
  const runtimeTokenPath = optionalText(env.MSWARM_SELF_HOSTED_NODE_KEY_PATH) || defaultRuntimeTokenPath();
@@ -215,11 +652,15 @@ export async function readSelfHostedNodeConfig(env = process.env) {
215
652
  return {
216
653
  gatewayBaseUrl: trimTrailingSlash(gatewayBaseUrl),
217
654
  nodeId,
655
+ serverName: optionalText(env.MSWARM_SELF_HOSTED_SERVER_NAME) || state.server_name || null,
656
+ relayMode: parseRelayMode(env.MSWARM_SELF_HOSTED_RELAY_MODE || state.relay_mode),
657
+ machineFingerprint: state.machine_fingerprint || null,
658
+ directBaseUrl: optionalText(env.MSWARM_SELF_HOSTED_DIRECT_BASE_URL) || state.direct_base_url || null,
218
659
  enrollmentToken: optionalText(env.MSWARM_SELF_HOSTED_ENROLLMENT_TOKEN),
219
660
  runtimeToken: optionalText(env.MSWARM_SELF_HOSTED_RUNTIME_TOKEN) || persistedRuntimeToken || state.runtime_token || null,
220
- discoveryMode: parseDiscoveryMode(env.MSWARM_SELF_HOSTED_DISCOVERY_MODE),
221
- mcodaBin: optionalText(env.MSWARM_SELF_HOSTED_MCODA_BIN) || DEFAULT_MCODA_BIN,
222
- mcodaListArgs: parseArgs(env.MSWARM_SELF_HOSTED_MCODA_LIST_ARGS, DEFAULT_MCODA_LIST_ARGS),
661
+ discoveryMode: parseDiscoveryMode(env.MSWARM_SELF_HOSTED_DISCOVERY_MODE || state.discovery_mode),
662
+ mcodaBin: optionalText(env.MSWARM_SELF_HOSTED_MCODA_BIN) || state.mcoda_bin || DEFAULT_MCODA_BIN,
663
+ mcodaListArgs: parseArgs(env.MSWARM_SELF_HOSTED_MCODA_LIST_ARGS || state.mcoda_list_args, DEFAULT_MCODA_LIST_ARGS),
223
664
  ollamaBaseUrl: trimTrailingSlash(ollamaBaseUrl),
224
665
  statePath,
225
666
  runtimeTokenPath,
@@ -227,12 +668,60 @@ export async function readSelfHostedNodeConfig(env = process.env) {
227
668
  optionalText(env.MSWARM_SELF_HOSTED_RELAY_SIGNING_SECRET),
228
669
  listenHost: optionalText(env.MSWARM_SELF_HOSTED_LISTEN_HOST) || DEFAULT_LISTEN_HOST,
229
670
  listenPort: parsePositiveInteger(env.MSWARM_SELF_HOSTED_LISTEN_PORT, DEFAULT_LISTEN_PORT),
230
- nodeVersion: optionalText(env.MSWARM_SELF_HOSTED_NODE_VERSION) || "0.1.0",
671
+ nodeVersion: optionalText(env.MSWARM_SELF_HOSTED_NODE_VERSION) || state.node_version || "0.1.1",
231
672
  heartbeatIntervalSeconds: parsePositiveInteger(env.MSWARM_SELF_HOSTED_HEARTBEAT_INTERVAL_SECONDS, state.heartbeat_interval_seconds || DEFAULT_HEARTBEAT_INTERVAL_SECONDS),
673
+ requestTimeoutMs: parsePositiveInteger(env.MSWARM_SELF_HOSTED_REQUEST_TIMEOUT_MS, state.request_timeout_ms || DEFAULT_REQUEST_TIMEOUT_MS),
674
+ exposeAllModels: parseBoolean(env.MSWARM_SELF_HOSTED_EXPOSE_ALL_MODELS, typeof state.expose_all_models === "boolean" ? state.expose_all_models : false),
675
+ modelAllowlist: parseList(env.MSWARM_SELF_HOSTED_MODEL_ALLOWLIST || state.model_allowlist),
676
+ modelBlocklist: parseList(env.MSWARM_SELF_HOSTED_MODEL_BLOCKLIST || state.model_blocklist)
677
+ };
678
+ }
679
+ export async function readOwnerSetupConfig(argv = process.argv.slice(3), env = process.env) {
680
+ const options = parseCliOptions(argv);
681
+ const statePath = optionalText(env.MSWARM_SELF_HOSTED_NODE_STATE_PATH) || defaultStatePath();
682
+ const runtimeTokenPath = optionalText(env.MSWARM_SELF_HOSTED_NODE_KEY_PATH) || defaultRuntimeTokenPath();
683
+ const gatewayBaseUrl = optionalText(options.gateway) ||
684
+ optionalText(env.MSWARM_GATEWAY_BASE_URL) ||
685
+ DEFAULT_SETUP_GATEWAY_BASE_URL;
686
+ const apiKey = optionalText(options["api-key"]) || optionalText(env.MSWARM_API_KEY) || "";
687
+ if (!apiKey) {
688
+ throw new Error("--api-key or MSWARM_API_KEY is required");
689
+ }
690
+ const relayMode = parseRelayMode(options.mode || env.MSWARM_SELF_HOSTED_RELAY_MODE);
691
+ const directBaseUrl = optionalText(options["direct-url"]) || optionalText(env.MSWARM_SELF_HOSTED_DIRECT_BASE_URL);
692
+ if (relayMode === "direct" && !directBaseUrl) {
693
+ throw new Error("--direct-url is required when --mode direct is used");
694
+ }
695
+ if (relayMode === "outbound" && directBaseUrl) {
696
+ throw new Error("--direct-url can only be used with --mode direct");
697
+ }
698
+ const ollamaBaseUrl = optionalText(env.MSWARM_SELF_HOSTED_OLLAMA_BASE_URL) ||
699
+ optionalText(env.OLLAMA_HOST) ||
700
+ DEFAULT_OLLAMA_BASE_URL;
701
+ const allowlist = parseList(options.allow || env.MSWARM_SELF_HOSTED_MODEL_ALLOWLIST);
702
+ const blocklist = parseList(options.block || env.MSWARM_SELF_HOSTED_MODEL_BLOCKLIST);
703
+ return {
704
+ apiKey,
705
+ gatewayBaseUrl: trimTrailingSlash(gatewayBaseUrl),
706
+ serverName: normalizeLocalName(optionalText(options["server-name"]) ||
707
+ optionalText(env.MSWARM_SELF_HOSTED_SERVER_NAME) ||
708
+ resolveDefaultServerName()),
709
+ relayMode,
710
+ directBaseUrl,
711
+ discoveryMode: parseDiscoveryMode(env.MSWARM_SELF_HOSTED_DISCOVERY_MODE),
712
+ statePath,
713
+ runtimeTokenPath,
714
+ machineIdPath: optionalText(env.MSWARM_SELF_HOSTED_MACHINE_ID_PATH) || defaultMachineIdPath(),
715
+ mcodaBin: optionalText(env.MSWARM_SELF_HOSTED_MCODA_BIN) || DEFAULT_MCODA_BIN,
716
+ mcodaListArgs: parseArgs(env.MSWARM_SELF_HOSTED_MCODA_LIST_ARGS, DEFAULT_MCODA_LIST_ARGS),
717
+ ollamaBaseUrl: trimTrailingSlash(ollamaBaseUrl),
718
+ nodeVersion: optionalText(env.MSWARM_SELF_HOSTED_NODE_VERSION) || "0.1.1",
719
+ heartbeatIntervalSeconds: parsePositiveInteger(env.MSWARM_SELF_HOSTED_HEARTBEAT_INTERVAL_SECONDS, DEFAULT_HEARTBEAT_INTERVAL_SECONDS),
232
720
  requestTimeoutMs: parsePositiveInteger(env.MSWARM_SELF_HOSTED_REQUEST_TIMEOUT_MS, DEFAULT_REQUEST_TIMEOUT_MS),
233
- exposeAllModels: parseBoolean(env.MSWARM_SELF_HOSTED_EXPOSE_ALL_MODELS, false),
234
- modelAllowlist: parseList(env.MSWARM_SELF_HOSTED_MODEL_ALLOWLIST),
235
- modelBlocklist: parseList(env.MSWARM_SELF_HOSTED_MODEL_BLOCKLIST)
721
+ exposeAllModels: options["expose-all"] === true || parseBoolean(env.MSWARM_SELF_HOSTED_EXPOSE_ALL_MODELS, false),
722
+ modelAllowlist: allowlist,
723
+ modelBlocklist: blocklist,
724
+ start: options.start === true
236
725
  };
237
726
  }
238
727
  export function mapOllamaModelToSelfHostedModel(model, config) {
@@ -380,10 +869,6 @@ async function defaultCommandRunner(command, args, options) {
380
869
  });
381
870
  }
382
871
  export class McodaAgentInventoryClient {
383
- command;
384
- args;
385
- timeoutMs;
386
- runner;
387
872
  constructor(input) {
388
873
  this.command = input.command || DEFAULT_MCODA_BIN;
389
874
  this.args = input.args?.length ? input.args : DEFAULT_MCODA_LIST_ARGS;
@@ -413,9 +898,6 @@ export class McodaAgentInventoryClient {
413
898
  }
414
899
  }
415
900
  export class OllamaClient {
416
- baseUrl;
417
- fetchImpl;
418
- timeoutMs;
419
901
  constructor(input) {
420
902
  this.baseUrl = trimTrailingSlash(input.baseUrl);
421
903
  this.fetchImpl = input.fetchImpl || fetch;
@@ -502,9 +984,6 @@ function buildOpenAIChatCompletion(input) {
502
984
  };
503
985
  }
504
986
  export class McodaLocalAgentExecutor {
505
- command;
506
- timeoutMs;
507
- runner;
508
987
  constructor(input) {
509
988
  this.command = input.command || DEFAULT_MCODA_BIN;
510
989
  this.timeoutMs = input.timeoutMs || DEFAULT_REQUEST_TIMEOUT_MS;
@@ -534,9 +1013,6 @@ export class McodaLocalAgentExecutor {
534
1013
  }
535
1014
  }
536
1015
  export class MswarmSelfHostedNodeClient {
537
- gatewayBaseUrl;
538
- fetchImpl;
539
- timeoutMs;
540
1016
  constructor(input) {
541
1017
  this.gatewayBaseUrl = trimTrailingSlash(input.gatewayBaseUrl);
542
1018
  this.fetchImpl = input.fetchImpl || fetch;
@@ -549,6 +1025,19 @@ export class MswarmSelfHostedNodeClient {
549
1025
  body: JSON.stringify({ node_id: nodeId, enrollment_token: enrollmentToken })
550
1026
  }, this.timeoutMs);
551
1027
  }
1028
+ async bootstrap(apiKey, payload) {
1029
+ return fetchJson(this.fetchImpl, `${this.gatewayBaseUrl}/v1/swarm/self-hosted/node/bootstrap`, {
1030
+ method: "POST",
1031
+ headers: {
1032
+ "content-type": "application/json",
1033
+ "x-api-key": apiKey
1034
+ },
1035
+ body: JSON.stringify(payload)
1036
+ }, this.timeoutMs);
1037
+ }
1038
+ async health() {
1039
+ return fetchJson(this.fetchImpl, `${this.gatewayBaseUrl}/healthz`, { method: "GET" }, this.timeoutMs);
1040
+ }
552
1041
  async heartbeat(runtimeToken, payload) {
553
1042
  return fetchJson(this.fetchImpl, `${this.gatewayBaseUrl}/v1/swarm/self-hosted/node/heartbeat`, {
554
1043
  method: "POST",
@@ -569,13 +1058,28 @@ export class MswarmSelfHostedNodeClient {
569
1058
  body: JSON.stringify(payload)
570
1059
  }, this.timeoutMs);
571
1060
  }
1061
+ async pollJob(runtimeToken, payload) {
1062
+ return fetchJson(this.fetchImpl, `${this.gatewayBaseUrl}/v1/swarm/self-hosted/node/jobs/poll`, {
1063
+ method: "POST",
1064
+ headers: {
1065
+ "content-type": "application/json",
1066
+ authorization: `Bearer ${runtimeToken}`
1067
+ },
1068
+ body: JSON.stringify(payload)
1069
+ }, Math.max(this.timeoutMs, (payload.wait_ms || 0) + 5000));
1070
+ }
1071
+ async postJobResult(runtimeToken, jobId, payload) {
1072
+ return fetchJson(this.fetchImpl, `${this.gatewayBaseUrl}/v1/swarm/self-hosted/node/jobs/${encodeURIComponent(jobId)}/result`, {
1073
+ method: "POST",
1074
+ headers: {
1075
+ "content-type": "application/json",
1076
+ authorization: `Bearer ${runtimeToken}`
1077
+ },
1078
+ body: JSON.stringify(payload)
1079
+ }, this.timeoutMs);
1080
+ }
572
1081
  }
573
1082
  export class SelfHostedNodeRuntime {
574
- config;
575
- gateway;
576
- mcoda;
577
- mcodaExecutor;
578
- ollama;
579
1083
  constructor(config, deps) {
580
1084
  this.config = config;
581
1085
  this.gateway =
@@ -606,6 +1110,96 @@ export class SelfHostedNodeRuntime {
606
1110
  timeoutMs: config.requestTimeoutMs
607
1111
  });
608
1112
  }
1113
+ static async setup(setupConfig, deps) {
1114
+ const gateway = deps?.gateway ||
1115
+ new MswarmSelfHostedNodeClient({
1116
+ gatewayBaseUrl: setupConfig.gatewayBaseUrl,
1117
+ fetchImpl: deps?.fetchImpl,
1118
+ timeoutMs: setupConfig.requestTimeoutMs
1119
+ });
1120
+ const machineId = await readOrCreateSelfHostedMachineId(setupConfig.machineIdPath);
1121
+ const machineFingerprint = machineFingerprintFromId(machineId);
1122
+ const bootstrap = await gateway.bootstrap(setupConfig.apiKey, {
1123
+ machine_fingerprint: machineFingerprint,
1124
+ server_name: setupConfig.serverName,
1125
+ label: setupConfig.serverName,
1126
+ relay_mode: setupConfig.relayMode,
1127
+ direct_base_url: setupConfig.directBaseUrl || null,
1128
+ node_version: setupConfig.nodeVersion,
1129
+ discovery_mode: setupConfig.discoveryMode,
1130
+ expose_all_models: setupConfig.exposeAllModels,
1131
+ model_allowlist: setupConfig.modelAllowlist,
1132
+ model_blocklist: setupConfig.modelBlocklist,
1133
+ heartbeat_interval_seconds: setupConfig.heartbeatIntervalSeconds
1134
+ });
1135
+ const nodeId = optionalText(bootstrap.node?.node_id);
1136
+ const runtimeToken = optionalText(bootstrap.runtime_token);
1137
+ if (!nodeId || !runtimeToken) {
1138
+ throw new Error("Bootstrap response did not include node_id and runtime_token");
1139
+ }
1140
+ const heartbeatInterval = bootstrap.heartbeat_interval_seconds || setupConfig.heartbeatIntervalSeconds;
1141
+ const state = {
1142
+ node_id: nodeId,
1143
+ server_name: optionalText(bootstrap.node?.server_name) || setupConfig.serverName,
1144
+ relay_mode: bootstrap.node?.relay_mode || setupConfig.relayMode,
1145
+ machine_fingerprint: machineFingerprint,
1146
+ direct_base_url: setupConfig.directBaseUrl || null,
1147
+ runtime_token: undefined,
1148
+ config_version: bootstrap.config_version,
1149
+ heartbeat_interval_seconds: heartbeatInterval,
1150
+ heartbeat_timeout_seconds: bootstrap.heartbeat_timeout_seconds,
1151
+ enrolled_at: new Date().toISOString(),
1152
+ updated_at: new Date().toISOString(),
1153
+ gateway_base_url: setupConfig.gatewayBaseUrl,
1154
+ ollama_base_url: setupConfig.ollamaBaseUrl,
1155
+ discovery_mode: setupConfig.discoveryMode,
1156
+ mcoda_bin: setupConfig.mcodaBin,
1157
+ mcoda_list_args: setupConfig.mcodaListArgs,
1158
+ node_version: setupConfig.nodeVersion,
1159
+ request_timeout_ms: setupConfig.requestTimeoutMs,
1160
+ expose_all_models: setupConfig.exposeAllModels,
1161
+ model_allowlist: setupConfig.modelAllowlist,
1162
+ model_blocklist: setupConfig.modelBlocklist
1163
+ };
1164
+ await writeSelfHostedNodeState(setupConfig.statePath, state);
1165
+ await writeSelfHostedRuntimeToken(setupConfig.runtimeTokenPath, runtimeToken);
1166
+ const runtime = new SelfHostedNodeRuntime({
1167
+ gatewayBaseUrl: setupConfig.gatewayBaseUrl,
1168
+ nodeId,
1169
+ serverName: state.server_name,
1170
+ relayMode: state.relay_mode || setupConfig.relayMode,
1171
+ machineFingerprint,
1172
+ directBaseUrl: setupConfig.directBaseUrl || null,
1173
+ enrollmentToken: null,
1174
+ runtimeToken,
1175
+ discoveryMode: setupConfig.discoveryMode,
1176
+ mcodaBin: setupConfig.mcodaBin,
1177
+ mcodaListArgs: setupConfig.mcodaListArgs,
1178
+ ollamaBaseUrl: setupConfig.ollamaBaseUrl,
1179
+ statePath: setupConfig.statePath,
1180
+ runtimeTokenPath: setupConfig.runtimeTokenPath,
1181
+ invocationSigningSecret: null,
1182
+ listenHost: DEFAULT_LISTEN_HOST,
1183
+ listenPort: DEFAULT_LISTEN_PORT,
1184
+ nodeVersion: setupConfig.nodeVersion,
1185
+ heartbeatIntervalSeconds: heartbeatInterval,
1186
+ requestTimeoutMs: setupConfig.requestTimeoutMs,
1187
+ exposeAllModels: setupConfig.exposeAllModels,
1188
+ modelAllowlist: setupConfig.modelAllowlist,
1189
+ modelBlocklist: setupConfig.modelBlocklist
1190
+ }, { ...deps, gateway });
1191
+ const once = await runtime.runOnce();
1192
+ return {
1193
+ created: bootstrap.created === true,
1194
+ nodeId,
1195
+ serverName: state.server_name || setupConfig.serverName,
1196
+ modelCount: once.model_count,
1197
+ status: once.status,
1198
+ statePath: setupConfig.statePath,
1199
+ runtimeTokenPath: setupConfig.runtimeTokenPath,
1200
+ start: setupConfig.start
1201
+ };
1202
+ }
609
1203
  async discoverModels() {
610
1204
  if (this.config.discoveryMode === "ollama") {
611
1205
  const [version, models] = await Promise.all([
@@ -642,7 +1236,15 @@ export class SelfHostedNodeRuntime {
642
1236
  enrolled_at: currentState.enrolled_at || new Date().toISOString(),
643
1237
  updated_at: new Date().toISOString(),
644
1238
  gateway_base_url: this.config.gatewayBaseUrl,
645
- ollama_base_url: this.config.ollamaBaseUrl
1239
+ ollama_base_url: this.config.ollamaBaseUrl,
1240
+ discovery_mode: this.config.discoveryMode,
1241
+ mcoda_bin: this.config.mcodaBin,
1242
+ mcoda_list_args: this.config.mcodaListArgs,
1243
+ node_version: this.config.nodeVersion,
1244
+ request_timeout_ms: this.config.requestTimeoutMs,
1245
+ expose_all_models: this.config.exposeAllModels,
1246
+ model_allowlist: this.config.modelAllowlist,
1247
+ model_blocklist: this.config.modelBlocklist
646
1248
  };
647
1249
  await writeSelfHostedNodeState(this.config.statePath, nextState);
648
1250
  await writeSelfHostedRuntimeToken(this.config.runtimeTokenPath, runtimeToken);
@@ -814,9 +1416,75 @@ export class SelfHostedNodeRuntime {
814
1416
  });
815
1417
  return { count: models.length, response };
816
1418
  }
1419
+ async pollAndExecuteJob(waitMs = DEFAULT_JOB_POLL_WAIT_MS) {
1420
+ const enrollment = await this.ensureEnrolled();
1421
+ const response = await this.gateway.pollJob(enrollment.runtimeToken, {
1422
+ node_id: this.config.nodeId,
1423
+ capacity: { active_jobs: 0, max_jobs: 1 },
1424
+ wait_ms: waitMs
1425
+ });
1426
+ const job = response.job || null;
1427
+ if (!job) {
1428
+ return { executed: false };
1429
+ }
1430
+ const result = await this.executeJob(job);
1431
+ await this.gateway.postJobResult(enrollment.runtimeToken, job.job_id, {
1432
+ ...result,
1433
+ node_id: this.config.nodeId
1434
+ });
1435
+ return { executed: true, job_id: job.job_id, status: result.status };
1436
+ }
1437
+ async doctor() {
1438
+ const checks = [];
1439
+ checks.push({ name: "config", ok: Boolean(this.config.nodeId), message: this.config.nodeId || "missing node id" });
1440
+ const runtimeToken = this.config.runtimeToken || (await readSelfHostedRuntimeToken(this.config.runtimeTokenPath));
1441
+ checks.push({ name: "runtime_token", ok: Boolean(runtimeToken), message: runtimeToken ? "present" : "missing" });
1442
+ try {
1443
+ await this.gateway.health();
1444
+ checks.push({ name: "gateway_health", ok: true });
1445
+ }
1446
+ catch (error) {
1447
+ checks.push({
1448
+ name: "gateway_health",
1449
+ ok: false,
1450
+ message: error instanceof Error ? error.message : String(error)
1451
+ });
1452
+ }
1453
+ try {
1454
+ const once = await this.runOnce();
1455
+ checks.push({ name: "heartbeat", ok: once.status === "online", message: once.status });
1456
+ checks.push({
1457
+ name: "local_agents",
1458
+ ok: once.model_count > 0,
1459
+ message: `${once.model_count} exposed local agent(s)`
1460
+ });
1461
+ }
1462
+ catch (error) {
1463
+ checks.push({
1464
+ name: "heartbeat",
1465
+ ok: false,
1466
+ message: error instanceof Error ? error.message : String(error)
1467
+ });
1468
+ }
1469
+ return { ok: checks.every((check) => check.ok), checks };
1470
+ }
817
1471
  startDaemon() {
818
1472
  let stopped = false;
819
1473
  let timer = null;
1474
+ let polling = false;
1475
+ const poll = () => {
1476
+ if (stopped || polling || this.config.relayMode === "direct")
1477
+ return;
1478
+ polling = true;
1479
+ void this.pollAndExecuteJob()
1480
+ .catch(() => undefined)
1481
+ .finally(() => {
1482
+ polling = false;
1483
+ if (!stopped) {
1484
+ setTimeout(poll, 0);
1485
+ }
1486
+ });
1487
+ };
820
1488
  const schedule = () => {
821
1489
  if (stopped)
822
1490
  return;
@@ -828,7 +1496,10 @@ export class SelfHostedNodeRuntime {
828
1496
  };
829
1497
  void this.runOnce()
830
1498
  .catch(() => undefined)
831
- .finally(schedule);
1499
+ .finally(() => {
1500
+ schedule();
1501
+ poll();
1502
+ });
832
1503
  return {
833
1504
  stop: () => {
834
1505
  stopped = true;