diff --git a/src/lib/agent-onboard.ts b/src/lib/agent-onboard.ts index 8d9f2701d8..8d4b67025c 100644 --- a/src/lib/agent-onboard.ts +++ b/src/lib/agent-onboard.ts @@ -8,12 +8,12 @@ import fs from "fs"; import os from "os"; import path from "path"; -import { spawnSync } from "child_process"; import { ROOT, run } from "./runner"; import { loadAgent, resolveAgentName, type AgentDefinition } from "./agent-defs"; import { getProviderSelectionConfig } from "./inference-config"; import * as onboardSession from "./onboard-session"; +import { sleepSeconds } from "./wait"; export interface OnboardContext { step: (current: number, total: number, message: string) => void; @@ -102,7 +102,7 @@ export function getAgentPermissivePolicyPath(agent: AgentDefinition): string | n } function sleep(seconds: number): void { - spawnSync("sleep", [String(seconds)]); + sleepSeconds(seconds); } /** diff --git a/src/lib/deploy.ts b/src/lib/deploy.ts index 1640f2b50e..9efe227176 100644 --- a/src/lib/deploy.ts +++ b/src/lib/deploy.ts @@ -5,6 +5,8 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; +import { sleepSeconds } from "./wait"; + export interface DeployCredentials { NVIDIA_API_KEY?: string | null; OPENAI_API_KEY?: string | null; @@ -333,7 +335,7 @@ export async function executeDeploy(opts: DeployExecutionOptions): Promise return fail([` Timed out waiting for Brev instance readiness for ${name}`], error, exit); } stdoutWrite("."); - spawnSync("sleep", ["3"]); + sleepSeconds(3); } // ── SSH trust-on-first-use (TOFU) ────────────────────────────── @@ -371,7 +373,7 @@ export async function executeDeploy(opts: DeployExecutionOptions): Promise ); } stdoutWrite("."); - spawnSync("sleep", ["3"]); + sleepSeconds(3); } const sshOpts = buildSshOpts(knownHostsFile, shellQuote); diff --git a/src/lib/nim.ts b/src/lib/nim.ts index d83e6399d2..421e0ae7da 100644 --- a/src/lib/nim.ts +++ b/src/lib/nim.ts @@ -6,6 +6,8 @@ // eslint-disable-next-line @typescript-eslint/no-require-imports const { run, runCapture } = require("./runner"); // eslint-disable-next-line @typescript-eslint/no-require-imports +const { sleepSeconds } = require("./wait"); +// eslint-disable-next-line @typescript-eslint/no-require-imports const nimImages = require("../../bin/lib/nim-images.json"); import { VLLM_PORT } from "./ports"; @@ -271,8 +273,7 @@ export function waitForNimHealth(port = VLLM_PORT, timeout = 300): boolean { } catch { /* ignored */ } - // eslint-disable-next-line @typescript-eslint/no-require-imports - require("child_process").spawnSync("sleep", [String(intervalSec)]); + sleepSeconds(intervalSec); } console.error(` NIM did not become healthy within ${timeout}s.`); return false; diff --git a/src/lib/onboard.ts b/src/lib/onboard.ts index 133d6aab09..899b94bba5 100644 --- a/src/lib/onboard.ts +++ b/src/lib/onboard.ts @@ -50,6 +50,9 @@ const { // Shared constant so getSuggestedPolicyPresets() and setupPoliciesWithSelection() // stay in sync. const LOCAL_INFERENCE_PROVIDERS = ["ollama-local", "vllm-local"]; +const { + sleepSeconds, +} = require("./wait"); const { inferContainerRuntime, isWsl, shouldPatchCoredns } = require("./platform"); const { resolveOpenshell } = require("./resolve-openshell"); const { @@ -2201,7 +2204,7 @@ function installOpenshell() { } function sleep(seconds) { - require("child_process").spawnSync("sleep", [String(seconds)]); + sleepSeconds(seconds); } function destroyGateway() { diff --git a/src/lib/wait.ts b/src/lib/wait.ts new file mode 100644 index 0000000000..e942893e83 --- /dev/null +++ b/src/lib/wait.ts @@ -0,0 +1,23 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +/** + * Synchronous waiting primitives for CLI commands. + */ + +/** + * Synchronously sleep for the given number of milliseconds. + * Uses Atomics.wait to block without pegging the CPU. + */ +export function sleepMs(ms: number): void { + if (ms <= 0 || !Number.isFinite(ms)) return; + const buffer = new Int32Array(new SharedArrayBuffer(4)); + Atomics.wait(buffer, 0, 0, ms); +} + +/** + * Synchronously sleep for the given number of seconds. + */ +export function sleepSeconds(seconds: number): void { + sleepMs(seconds * 1000); +} diff --git a/src/nemoclaw.ts b/src/nemoclaw.ts index 9b3cd89a71..38ce548133 100644 --- a/src/nemoclaw.ts +++ b/src/nemoclaw.ts @@ -67,6 +67,7 @@ const sandboxVersion = require("./lib/sandbox-version"); const sandboxState = require("./lib/sandbox-state"); const { ensureOllamaAuthProxy } = require("./lib/onboard"); const skillInstall = require("./lib/skill-install"); +const { sleepSeconds } = require("./lib/wait"); const { parseSandboxPhase } = require("./lib/gateway-state"); // ── Global commands ────────────────────────────────────────────── @@ -317,7 +318,7 @@ function checkAndRecoverSandboxProcesses(sandboxName, { quiet = false } = {}) { const recovered = recoverSandboxProcesses(sandboxName); if (recovered) { // Wait for gateway to bind its HTTP port before declaring success - spawnSync("sleep", ["3"]); + sleepSeconds(3); if (isSandboxGatewayRunning(sandboxName) !== true) { // Gateway process started but HTTP endpoint never came up if (!quiet) { diff --git a/test/wait.test.ts b/test/wait.test.ts new file mode 100644 index 0000000000..b630874682 --- /dev/null +++ b/test/wait.test.ts @@ -0,0 +1,41 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import assert from "node:assert"; +import { describe, expect, it } from "vitest"; +import { sleepMs, sleepSeconds } from "../src/lib/wait.js"; + +describe("wait utility", () => { + it("sleepMs blocks for approximately the requested time", () => { + const start = performance.now(); + sleepMs(100); + const end = performance.now(); + const duration = end - start; + + // Allow for some jitter, but should be at least 100ms. + // Increased upper bound to 500ms to avoid CI flakes on loaded runners. + assert.ok(duration >= 100, `duration ${duration}ms < 100ms`); + assert.ok(duration < 500, `duration ${duration}ms > 500ms`); + }); + + it("sleepSeconds blocks for approximately the requested time", () => { + const start = performance.now(); + sleepSeconds(0.1); + const end = performance.now(); + const duration = end - start; + + assert.ok(duration >= 100, `duration ${duration}ms < 100ms`); + assert.ok(duration < 500, `duration ${duration}ms > 500ms`); + }); + + it("returns immediately for zero, negative, or non-finite time", () => { + const start = performance.now(); + sleepMs(0); + sleepMs(-50); + sleepMs(NaN); + sleepMs(Infinity); + const end = performance.now(); + const duration = end - start; + assert.ok(duration < 50, `duration ${duration}ms > 50ms`); + }); +});