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
8 changes: 8 additions & 0 deletions .agents/skills/nemoclaw-user-configure-inference/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,16 +127,24 @@ To change these values, set the corresponding environment variables before runni
| `NEMOCLAW_CONTEXT_WINDOW` | Positive integer (tokens) | `131072` |
| `NEMOCLAW_MAX_TOKENS` | Positive integer (tokens) | `4096` |
| `NEMOCLAW_REASONING` | `true` or `false` | `false` |
| `NEMOCLAW_AGENT_TIMEOUT` | Positive integer (seconds) | `600` |

Invalid values are ignored, and the default bakes into the image.

```console
$ export NEMOCLAW_CONTEXT_WINDOW=65536
$ export NEMOCLAW_MAX_TOKENS=8192
$ export NEMOCLAW_REASONING=true
$ export NEMOCLAW_AGENT_TIMEOUT=1800
$ nemoclaw onboard
```

`NEMOCLAW_AGENT_TIMEOUT` controls the per-request inference timeout baked into
`agents.defaults.timeoutSeconds`. Increase it for slow local inference (for
example, CPU-only Ollama or vLLM on modest hardware). `openclaw.json` is
immutable at runtime, so this value can only be changed by rebuilding the
sandbox via `nemoclaw onboard`.

These variables are build-time settings.
If you change them on an existing sandbox, recreate the sandbox so the new values bake into the image:

Expand Down
10 changes: 9 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,11 @@ ARG NEMOCLAW_INFERENCE_API=openai-completions
ARG NEMOCLAW_CONTEXT_WINDOW=131072
ARG NEMOCLAW_MAX_TOKENS=4096
ARG NEMOCLAW_REASONING=false
# Per-request inference timeout (seconds) baked into agents.defaults.timeoutSeconds.
# Increase for slow local inference (e.g., CPU Ollama). openclaw.json is
# immutable at runtime (Landlock read-only), so this can only be changed by
# rebuilding via `nemoclaw onboard`. Ref: issue #2281
ARG NEMOCLAW_AGENT_TIMEOUT=600
ARG NEMOCLAW_INFERENCE_COMPAT_B64=e30=
# Base64-encoded JSON list of messaging channel names to pre-configure
# (e.g. ["discord","telegram"]). Channels are added with placeholder tokens
Expand Down Expand Up @@ -209,6 +214,7 @@ ENV NEMOCLAW_MODEL=${NEMOCLAW_MODEL} \
NEMOCLAW_CONTEXT_WINDOW=${NEMOCLAW_CONTEXT_WINDOW} \
NEMOCLAW_MAX_TOKENS=${NEMOCLAW_MAX_TOKENS} \
NEMOCLAW_REASONING=${NEMOCLAW_REASONING} \
NEMOCLAW_AGENT_TIMEOUT=${NEMOCLAW_AGENT_TIMEOUT} \
NEMOCLAW_INFERENCE_COMPAT_B64=${NEMOCLAW_INFERENCE_COMPAT_B64} \
NEMOCLAW_MESSAGING_CHANNELS_B64=${NEMOCLAW_MESSAGING_CHANNELS_B64} \
NEMOCLAW_MESSAGING_ALLOWED_IDS_B64=${NEMOCLAW_MESSAGING_ALLOWED_IDS_B64} \
Expand Down Expand Up @@ -250,6 +256,8 @@ inference_api = os.environ['NEMOCLAW_INFERENCE_API']; \
context_window = int(os.environ.get('NEMOCLAW_CONTEXT_WINDOW', '131072')); \
max_tokens = int(os.environ.get('NEMOCLAW_MAX_TOKENS', '4096')); \
reasoning = os.environ.get('NEMOCLAW_REASONING', 'false') == 'true'; \
_raw_agent_timeout = os.environ.get('NEMOCLAW_AGENT_TIMEOUT', '600'); \
agent_timeout = int(_raw_agent_timeout) if _raw_agent_timeout.isdigit() and int(_raw_agent_timeout) > 0 else (_ for _ in ()).throw(ValueError('NEMOCLAW_AGENT_TIMEOUT must be a positive integer')); \
inference_compat = json.loads(base64.b64decode(os.environ['NEMOCLAW_INFERENCE_COMPAT_B64']).decode('utf-8')); \
msg_channels = json.loads(base64.b64decode(os.environ.get('NEMOCLAW_MESSAGING_CHANNELS_B64', 'W10=') or 'W10=').decode('utf-8')); \
_allowed_ids = json.loads(base64.b64decode(os.environ.get('NEMOCLAW_MESSAGING_ALLOWED_IDS_B64', 'e30=') or 'e30=').decode('utf-8')); \
Expand All @@ -273,7 +281,7 @@ providers = { \
} \
}; \
config = { \
'agents': {'defaults': {'model': {'primary': primary_model_ref}}}, \
'agents': {'defaults': {'model': {'primary': primary_model_ref}, 'timeoutSeconds': agent_timeout}}, \
'models': {'mode': 'merge', 'providers': providers}, \
'channels': {'defaults': {}, **_ch_cfg}, \
'update': {'checkOnStart': False}, \
Expand Down
8 changes: 8 additions & 0 deletions docs/inference/switch-inference-providers.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,16 +141,24 @@ To change these values, set the corresponding environment variables before runni
| `NEMOCLAW_CONTEXT_WINDOW` | Positive integer (tokens) | `131072` |
| `NEMOCLAW_MAX_TOKENS` | Positive integer (tokens) | `4096` |
| `NEMOCLAW_REASONING` | `true` or `false` | `false` |
| `NEMOCLAW_AGENT_TIMEOUT` | Positive integer (seconds) | `600` |

Invalid values are ignored, and the default bakes into the image.

```console
$ export NEMOCLAW_CONTEXT_WINDOW=65536
$ export NEMOCLAW_MAX_TOKENS=8192
$ export NEMOCLAW_REASONING=true
$ export NEMOCLAW_AGENT_TIMEOUT=1800
$ nemoclaw onboard
```

`NEMOCLAW_AGENT_TIMEOUT` controls the per-request inference timeout baked into
`agents.defaults.timeoutSeconds`. Increase it for slow local inference (for
example, CPU-only Ollama or vLLM on modest hardware). `openclaw.json` is
immutable at runtime, so this value can only be changed by rebuilding the
sandbox via `nemoclaw onboard`.

These variables are build-time settings.
If you change them on an existing sandbox, recreate the sandbox so the new values bake into the image:

Expand Down
14 changes: 13 additions & 1 deletion src/lib/onboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1395,6 +1395,16 @@ function patchStagedDockerfile(
`ARG NEMOCLAW_REASONING=${reasoning}`,
);
}
// NEMOCLAW_AGENT_TIMEOUT — override agents.defaults.timeoutSeconds at build
// time. Lets users increase the per-request inference timeout without
// editing the Dockerfile. Ref: issue #2281
const agentTimeout = process.env.NEMOCLAW_AGENT_TIMEOUT;
if (agentTimeout && POSITIVE_INT_RE.test(agentTimeout)) {
dockerfile = dockerfile.replace(
/^ARG NEMOCLAW_AGENT_TIMEOUT=.*$/m,
`ARG NEMOCLAW_AGENT_TIMEOUT=${agentTimeout}`,
);
}
// Honor NEMOCLAW_PROXY_HOST / NEMOCLAW_PROXY_PORT exported in the host
// shell so the sandbox-side nemoclaw-start.sh sees them via $ENV at runtime.
// Without this, the host export is silently dropped at image build time and
Expand Down Expand Up @@ -6405,7 +6415,9 @@ async function onboard(opts = {}) {
session = onboardSession.loadSession();
if (!session || session.resumable === false) {
console.error(" No resumable onboarding session was found.");
console.error(" Run: nemoclaw onboard");
console.error(" --resume only continues an interrupted onboarding run.");
console.error(" To change configuration on an existing sandbox, rebuild it:");
console.error(" nemoclaw onboard");
process.exit(1);
}
const sessionFrom = session?.metadata?.fromDockerfile || null;
Expand Down
9 changes: 9 additions & 0 deletions test/cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,15 @@ describe("CLI dispatch", () => {
expect(r.out.includes("No resumable onboarding session was found")).toBeTruthy();
});

it("resume rejection clarifies --resume semantics and points to onboard (#2281)", () => {
const r = run("onboard --resume --non-interactive --yes-i-accept-third-party-software");
expect(r.code).toBe(1);
expect(r.out.includes("No resumable onboarding session was found")).toBeTruthy();
expect(r.out.includes("--resume only continues an interrupted onboarding run")).toBeTruthy();
expect(r.out.includes("To change configuration on an existing sandbox, rebuild it")).toBeTruthy();
expect(r.out.includes("nemoclaw onboard")).toBeTruthy();
});

it("setup-spark --help exits 0 and shows onboard usage", () => {
const r = run("setup-spark --help");
expect(r.code).toBe(0);
Expand Down
86 changes: 86 additions & 0 deletions test/onboard.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -753,6 +753,92 @@ describe("onboard helpers", () => {
}
});

it("#2281: bakes NEMOCLAW_AGENT_TIMEOUT env into the staged Dockerfile", () => {
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-onboard-dockerfile-timeout-"));
const dockerfilePath = path.join(tmpDir, "Dockerfile");
fs.writeFileSync(
dockerfilePath,
[
"ARG NEMOCLAW_MODEL=nvidia/nemotron-3-super-120b-a12b",
"ARG NEMOCLAW_PROVIDER_KEY=nvidia",
"ARG NEMOCLAW_PRIMARY_MODEL_REF=nvidia/nemotron-3-super-120b-a12b",
"ARG CHAT_UI_URL=http://127.0.0.1:18789",
"ARG NEMOCLAW_INFERENCE_BASE_URL=https://inference.local/v1",
"ARG NEMOCLAW_INFERENCE_API=openai-completions",
"ARG NEMOCLAW_INFERENCE_COMPAT_B64=e30=",
"ARG NEMOCLAW_WEB_SEARCH_ENABLED=0",
"ARG NEMOCLAW_BUILD_ID=default",
"ARG NEMOCLAW_AGENT_TIMEOUT=600",
].join("\n"),
);

const priorTimeout = process.env.NEMOCLAW_AGENT_TIMEOUT;
process.env.NEMOCLAW_AGENT_TIMEOUT = "1800";
try {
patchStagedDockerfile(
dockerfilePath,
"gpt-5.4",
"http://127.0.0.1:18789",
"build-timeout",
"openai-api",
);
const patched = fs.readFileSync(dockerfilePath, "utf8");
assert.match(patched, /^ARG NEMOCLAW_AGENT_TIMEOUT=1800$/m);
} finally {
if (priorTimeout === undefined) {
delete process.env.NEMOCLAW_AGENT_TIMEOUT;
} else {
process.env.NEMOCLAW_AGENT_TIMEOUT = priorTimeout;
}
fs.rmSync(tmpDir, { recursive: true, force: true });
}
});

it("#2281: rejects malformed NEMOCLAW_AGENT_TIMEOUT and keeps default", () => {
const tmpDir = fs.mkdtempSync(
path.join(os.tmpdir(), "nemoclaw-onboard-dockerfile-timeout-default-"),
);
const dockerfilePath = path.join(tmpDir, "Dockerfile");
fs.writeFileSync(
dockerfilePath,
[
"ARG NEMOCLAW_MODEL=nvidia/nemotron-3-super-120b-a12b",
"ARG NEMOCLAW_PROVIDER_KEY=nvidia",
"ARG NEMOCLAW_PRIMARY_MODEL_REF=nvidia/nemotron-3-super-120b-a12b",
"ARG CHAT_UI_URL=http://127.0.0.1:18789",
"ARG NEMOCLAW_INFERENCE_BASE_URL=https://inference.local/v1",
"ARG NEMOCLAW_INFERENCE_API=openai-completions",
"ARG NEMOCLAW_INFERENCE_COMPAT_B64=e30=",
"ARG NEMOCLAW_WEB_SEARCH_ENABLED=0",
"ARG NEMOCLAW_BUILD_ID=default",
"ARG NEMOCLAW_AGENT_TIMEOUT=600",
].join("\n"),
);

const priorTimeout = process.env.NEMOCLAW_AGENT_TIMEOUT;
// Malformed: not a positive integer. Should be rejected, default kept.
process.env.NEMOCLAW_AGENT_TIMEOUT = "not-a-number\nRUN rm -rf /";
try {
patchStagedDockerfile(
dockerfilePath,
"gpt-5.4",
"http://127.0.0.1:18789",
"build-timeout-bad",
"openai-api",
);
const patched = fs.readFileSync(dockerfilePath, "utf8");
assert.match(patched, /^ARG NEMOCLAW_AGENT_TIMEOUT=600$/m);
assert.doesNotMatch(patched, /RUN rm -rf/);
} finally {
if (priorTimeout === undefined) {
delete process.env.NEMOCLAW_AGENT_TIMEOUT;
} else {
process.env.NEMOCLAW_AGENT_TIMEOUT = priorTimeout;
}
fs.rmSync(tmpDir, { recursive: true, force: true });
}
});

it("patches the staged Dockerfile with Brave Search config when enabled", () => {
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-onboard-dockerfile-web-"));
const dockerfilePath = path.join(tmpDir, "Dockerfile");
Expand Down
Loading