Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions docs/inference/configure-inference-timeouts.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ Use the error location to select the correct setting.

The readiness timeout does not govern inference requests or provider validation.

Set each value to a whole number of seconds; a fractional value is rounded.
NemoClaw falls back to the default shown above for any value it cannot use, including a negative one — a negative budget is treated as an invalid setting rather than as a request to give up immediately.

## Increase the OpenClaw Request Timeout

<AgentOnly variant="openclaw">
Expand Down
72 changes: 72 additions & 0 deletions src/lib/onboard/env-int.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

// `envInt` backs the poll counts, poll intervals and readiness budgets used
// across onboarding and gateway recovery. A negative override used to be
// clamped to 0, which is the most damaging reading available for every one of
// those knobs, while any other invalid value already fell back (#7881).

import { describe, expect, it, vi } from "vitest";

import { envInt } from "./env";

const FALLBACK = 30;

function read(value: string | undefined): number {
return envInt("NEMOCLAW_TEST_KNOB", FALLBACK, { NEMOCLAW_TEST_KNOB: value });
}

describe("envInt override parsing", () => {
it.each([
"-1",
"-30",
"-0.4",
"-1e3",
])("falls back instead of collapsing a negative override (%s) to zero", (value) => {
expect(read(value)).toBe(FALLBACK);
});

it.each([
"abc",
"NaN",
"Infinity",
"-Infinity",
])("keeps falling back for a non-finite override (%s)", (value) => {
expect(read(value)).toBe(FALLBACK);
});

it.each([
["unset", undefined],
["empty", ""],
])("keeps falling back for an %s override", (_label, value) => {
expect(read(value)).toBe(FALLBACK);
});

it("still accepts an explicit zero", () => {
// Regression lock: callers that read 0 as "disabled" or clamp it upward
// themselves must keep seeing 0, so this fix cannot change their meaning.
expect(read("0")).toBe(0);
});

it.each([
["3", 3],
["0.4", 0],
["2.6", 3],
["600", 600],
])("keeps rounding a valid override (%s)", (value, expected) => {
expect(read(value)).toBe(expected);
});

it("uses the supplied env map rather than the process environment", () => {
// Both assertions need a conflicting process value to have any teeth: with
// `process.env` unset, an implementation that ignored the supplied map
// would return the fallback here and still pass.
vi.stubEnv("NEMOCLAW_TEST_KNOB", "999");
try {
expect(envInt("NEMOCLAW_TEST_KNOB", FALLBACK, { NEMOCLAW_TEST_KNOB: "7" })).toBe(7);
expect(envInt("NEMOCLAW_TEST_KNOB", FALLBACK, {})).toBe(FALLBACK);
} finally {
vi.unstubAllEnvs();
}
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.
});
9 changes: 8 additions & 1 deletion src/lib/onboard/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,14 @@ export function envInt(
const raw = env[name];
if (raw === undefined || raw === "") return fallback;
const n = Number(raw);
return Number.isFinite(n) ? Math.max(0, Math.round(n)) : fallback;
// A negative override is invalid input, not a request for zero. Clamping it
// to 0 picked the most damaging reading available for these knobs -- an
// empty poll loop, a zero-second readiness budget, a timeout that expires
// before its first attempt -- while any other unparseable value already fell
// back to the caller's default. Treat both the same way, which is what the
// sibling `readNonNegativeNumberEnv` has always done (#7881).
if (!Number.isFinite(n) || n < 0) return fallback;
return Math.round(n);
}

/** Inference timeout (seconds) for local providers (Ollama, vLLM, NIM). */
Expand Down
10 changes: 9 additions & 1 deletion src/lib/onboard/sandbox-readiness-tracing.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -404,13 +404,21 @@ describe("getSandboxReadyErrorDebouncePolls env contract", () => {

it("clamps to a minimum of 1 poll", () => {
expect(getSandboxReadyErrorDebouncePolls({ [SANDBOX_READY_ERROR_DEBOUNCE_ENV]: "0" })).toBe(1);
expect(getSandboxReadyErrorDebouncePolls({ [SANDBOX_READY_ERROR_DEBOUNCE_ENV]: "-5" })).toBe(1);
// envInt rounds 0.4 -> 0, then the clamp lifts it to 1.
expect(getSandboxReadyErrorDebouncePolls({ [SANDBOX_READY_ERROR_DEBOUNCE_ENV]: "0.4" })).toBe(
1,
);
});

it("falls back for a negative override instead of clamping it to the minimum", () => {
// A negative is invalid input, so it reaches the documented default the
// same way "abc" does above, rather than silently becoming the smallest
// legal debounce (#7881).
expect(getSandboxReadyErrorDebouncePolls({ [SANDBOX_READY_ERROR_DEBOUNCE_ENV]: "-5" })).toBe(
30,
);
});

it("rounds fractional env values (envInt semantics)", () => {
expect(getSandboxReadyErrorDebouncePolls({ [SANDBOX_READY_ERROR_DEBOUNCE_ENV]: "2.6" })).toBe(
3,
Expand Down
Loading