From fc2276f64b97d95e23315d3d86fa331bfa604213 Mon Sep 17 00:00:00 2001 From: Krish Sapru Date: Fri, 17 Apr 2026 13:03:06 -0400 Subject: [PATCH 1/4] refactor(cli): replace spawnSync("sleep") with native wait utility --- src/lib/agent-onboard.ts | 4 ++-- src/lib/deploy.ts | 4 ++-- src/lib/nim.ts | 4 +++- src/lib/onboard.ts | 3 +++ src/lib/wait.ts | 23 +++++++++++++++++++++++ src/nemoclaw.ts | 3 ++- test/wait.test.ts | 38 ++++++++++++++++++++++++++++++++++++++ 7 files changed, 73 insertions(+), 6 deletions(-) create mode 100644 src/lib/wait.ts create mode 100644 test/wait.test.ts diff --git a/src/lib/agent-onboard.ts b/src/lib/agent-onboard.ts index bc5e6648232..3fe9e25f8d0 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, shellQuote } 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; @@ -100,7 +100,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 1640f2b50e6..917b839799b 100644 --- a/src/lib/deploy.ts +++ b/src/lib/deploy.ts @@ -333,7 +333,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 +371,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 d30db81fa42..469785ac85a 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"; @@ -231,7 +233,7 @@ export function waitForNimHealth(port = VLLM_PORT, timeout = 300): boolean { /* 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 a8221aa687e..975e7789407 100644 --- a/src/lib/onboard.ts +++ b/src/lib/onboard.ts @@ -51,6 +51,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 { diff --git a/src/lib/wait.ts b/src/lib/wait.ts new file mode 100644 index 00000000000..54fb3c5c45a --- /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) 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 2f7ec71c1dd..197b810cc72 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"); // ── Global commands ────────────────────────────────────────────── @@ -316,7 +317,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 00000000000..edfb925229a --- /dev/null +++ b/test/wait.test.ts @@ -0,0 +1,38 @@ +// 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.ts"; + +describe("wait utility", () => { + it("sleepMs blocks for approximately the requested time", () => { + const start = Date.now(); + sleepMs(100); + const end = Date.now(); + const duration = end - start; + + // Allow for some jitter, but should be at least 100ms and not excessively more. + assert.ok(duration >= 100, `duration ${duration}ms < 100ms`); + assert.ok(duration < 200, `duration ${duration}ms > 200ms`); + }); + + it("sleepSeconds blocks for approximately the requested time", () => { + const start = Date.now(); + sleepSeconds(0.1); + const end = Date.now(); + const duration = end - start; + + assert.ok(duration >= 100, `duration ${duration}ms < 100ms`); + assert.ok(duration < 200, `duration ${duration}ms > 200ms`); + }); + + it("returns immediately for zero or negative time", () => { + const start = Date.now(); + sleepMs(0); + sleepMs(-50); + const end = Date.now(); + const duration = end - start; + assert.ok(duration < 50, `duration ${duration}ms > 50ms`); + }); +}); From d38e1dd58a5bdf923adc579f08af5563e73cbb76 Mon Sep 17 00:00:00 2001 From: Krish Sapru Date: Fri, 17 Apr 2026 15:12:51 -0400 Subject: [PATCH 2/4] fix(cli): address review feedback for native wait utility --- src/lib/deploy.ts | 2 ++ src/lib/onboard.ts | 2 +- src/lib/wait.ts | 2 +- test/wait.test.ts | 11 +++++++---- 4 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/lib/deploy.ts b/src/lib/deploy.ts index 917b839799b..9efe2271765 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; diff --git a/src/lib/onboard.ts b/src/lib/onboard.ts index 975e7789407..862615ee250 100644 --- a/src/lib/onboard.ts +++ b/src/lib/onboard.ts @@ -2119,7 +2119,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 index 54fb3c5c45a..e942893e83f 100644 --- a/src/lib/wait.ts +++ b/src/lib/wait.ts @@ -10,7 +10,7 @@ * Uses Atomics.wait to block without pegging the CPU. */ export function sleepMs(ms: number): void { - if (ms <= 0) return; + if (ms <= 0 || !Number.isFinite(ms)) return; const buffer = new Int32Array(new SharedArrayBuffer(4)); Atomics.wait(buffer, 0, 0, ms); } diff --git a/test/wait.test.ts b/test/wait.test.ts index edfb925229a..498cadd7036 100644 --- a/test/wait.test.ts +++ b/test/wait.test.ts @@ -12,9 +12,10 @@ describe("wait utility", () => { const end = Date.now(); const duration = end - start; - // Allow for some jitter, but should be at least 100ms and not excessively more. + // 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 < 200, `duration ${duration}ms > 200ms`); + assert.ok(duration < 500, `duration ${duration}ms > 500ms`); }); it("sleepSeconds blocks for approximately the requested time", () => { @@ -24,13 +25,15 @@ describe("wait utility", () => { const duration = end - start; assert.ok(duration >= 100, `duration ${duration}ms < 100ms`); - assert.ok(duration < 200, `duration ${duration}ms > 200ms`); + assert.ok(duration < 500, `duration ${duration}ms > 500ms`); }); - it("returns immediately for zero or negative time", () => { + it("returns immediately for zero, negative, or non-finite time", () => { const start = Date.now(); sleepMs(0); sleepMs(-50); + sleepMs(NaN); + sleepMs(Infinity); const end = Date.now(); const duration = end - start; assert.ok(duration < 50, `duration ${duration}ms > 50ms`); From b3737e7b6c0c0616f241ede9c300b6ce256e044e Mon Sep 17 00:00:00 2001 From: Krish Sapru Date: Fri, 17 Apr 2026 15:32:40 -0400 Subject: [PATCH 3/4] test(wait): use performance.now() for monotonic timing stabilization --- test/wait.test.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/test/wait.test.ts b/test/wait.test.ts index 498cadd7036..e9cbe08f039 100644 --- a/test/wait.test.ts +++ b/test/wait.test.ts @@ -7,9 +7,9 @@ import { sleepMs, sleepSeconds } from "../src/lib/wait.ts"; describe("wait utility", () => { it("sleepMs blocks for approximately the requested time", () => { - const start = Date.now(); + const start = performance.now(); sleepMs(100); - const end = Date.now(); + const end = performance.now(); const duration = end - start; // Allow for some jitter, but should be at least 100ms. @@ -19,9 +19,9 @@ describe("wait utility", () => { }); it("sleepSeconds blocks for approximately the requested time", () => { - const start = Date.now(); + const start = performance.now(); sleepSeconds(0.1); - const end = Date.now(); + const end = performance.now(); const duration = end - start; assert.ok(duration >= 100, `duration ${duration}ms < 100ms`); @@ -29,12 +29,12 @@ describe("wait utility", () => { }); it("returns immediately for zero, negative, or non-finite time", () => { - const start = Date.now(); + const start = performance.now(); sleepMs(0); sleepMs(-50); sleepMs(NaN); sleepMs(Infinity); - const end = Date.now(); + const end = performance.now(); const duration = end - start; assert.ok(duration < 50, `duration ${duration}ms > 50ms`); }); From 4aeed39f56ba6a356caa2e88780afc8aebf5154e Mon Sep 17 00:00:00 2001 From: Krish Sapru Date: Tue, 21 Apr 2026 09:07:09 -0400 Subject: [PATCH 4/4] fix(cli): resolve import extension and stale eslint comments from review --- src/lib/nim.ts | 1 - test/wait.test.ts | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/lib/nim.ts b/src/lib/nim.ts index e5901df7642..421e0ae7dae 100644 --- a/src/lib/nim.ts +++ b/src/lib/nim.ts @@ -273,7 +273,6 @@ export function waitForNimHealth(port = VLLM_PORT, timeout = 300): boolean { } catch { /* ignored */ } - // eslint-disable-next-line @typescript-eslint/no-require-imports sleepSeconds(intervalSec); } console.error(` NIM did not become healthy within ${timeout}s.`); diff --git a/test/wait.test.ts b/test/wait.test.ts index e9cbe08f039..b6308746821 100644 --- a/test/wait.test.ts +++ b/test/wait.test.ts @@ -3,7 +3,7 @@ import assert from "node:assert"; import { describe, expect, it } from "vitest"; -import { sleepMs, sleepSeconds } from "../src/lib/wait.ts"; +import { sleepMs, sleepSeconds } from "../src/lib/wait.js"; describe("wait utility", () => { it("sleepMs blocks for approximately the requested time", () => {