Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
7 changes: 7 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,12 @@ ARG NEMOCLAW_INFERENCE_INPUTS=text
# 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
# Cadence for OpenClaw's periodic heartbeat
# (agents.defaults.heartbeat.every). Accepts Go-style durations like "30m",
# "5m", "1h"; "0m" disables heartbeat. Empty default preserves the OpenClaw
# built-in cadence. openclaw.json is immutable at runtime, so this can only
# change at image build time. Ref: issue #2880
ARG NEMOCLAW_AGENT_HEARTBEAT_EVERY=
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 @@ -314,6 +320,7 @@ ENV NEMOCLAW_MODEL=${NEMOCLAW_MODEL} \
NEMOCLAW_REASONING=${NEMOCLAW_REASONING} \
NEMOCLAW_INFERENCE_INPUTS=${NEMOCLAW_INFERENCE_INPUTS} \
NEMOCLAW_AGENT_TIMEOUT=${NEMOCLAW_AGENT_TIMEOUT} \
NEMOCLAW_AGENT_HEARTBEAT_EVERY=${NEMOCLAW_AGENT_HEARTBEAT_EVERY} \
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
9 changes: 9 additions & 0 deletions docs/inference/switch-inference-providers.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ To change these values, set the corresponding environment variables before runni
| `NEMOCLAW_REASONING` | `true` or `false` | `false` |
| `NEMOCLAW_INFERENCE_INPUTS` | `text` or `text,image` | `text` |
| `NEMOCLAW_AGENT_TIMEOUT` | Positive integer (seconds) | `600` |
| `NEMOCLAW_AGENT_HEARTBEAT_EVERY` | Go-style duration (`30m`, `1h`, `0m` to disable) | `unset` (OpenClaw default) |

Invalid values are ignored, and the default bakes into the image.
Use `NEMOCLAW_INFERENCE_INPUTS=text,image` only for a model that accepts image input through the selected provider.
Expand All @@ -156,6 +157,7 @@ $ export NEMOCLAW_MAX_TOKENS=8192
$ export NEMOCLAW_REASONING=true
$ export NEMOCLAW_INFERENCE_INPUTS=text,image
$ export NEMOCLAW_AGENT_TIMEOUT=1800
$ export NEMOCLAW_AGENT_HEARTBEAT_EVERY=0m
$ nemoclaw onboard
```

Expand All @@ -165,6 +167,13 @@ 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`.

`NEMOCLAW_AGENT_HEARTBEAT_EVERY` sets `agents.defaults.heartbeat.every`. Set it
to `0m` to disable the periodic heartbeat when it disrupts long-running agent
turns; leave it unset to preserve the OpenClaw default cadence. `openclaw.json`
is immutable at runtime, so the in-sandbox `openclaw config set` command cannot
change this — rebuild the sandbox via `nemoclaw onboard --resume` to apply a
new value.

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
21 changes: 21 additions & 0 deletions scripts/generate-openclaw-config.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
NEMOCLAW_MAX_TOKENS Max tokens (default: 4096)
NEMOCLAW_REASONING Enable reasoning (default: false)
NEMOCLAW_AGENT_TIMEOUT Per-request timeout seconds (default: 600)
NEMOCLAW_AGENT_HEARTBEAT_EVERY OpenClaw agent heartbeat cadence (e.g. "30m", "0m" to
disable). Empty/unset preserves the OpenClaw default.
NEMOCLAW_INFERENCE_COMPAT_B64 Base64-encoded inference compat JSON
NEMOCLAW_MESSAGING_CHANNELS_B64 Base64-encoded channel list
NEMOCLAW_MESSAGING_ALLOWED_IDS_B64 Base64-encoded allowed IDs map
Expand Down Expand Up @@ -336,6 +338,20 @@ def build_config(env: dict | None = None) -> dict:
raise ValueError("NEMOCLAW_AGENT_TIMEOUT must be a positive integer")
agent_timeout = int(_raw_agent_timeout)

# NemoClaw#2880: expose OpenClaw's agents.defaults.heartbeat.every so users
# can disable the periodic heartbeat (e.g. "0m") without editing
# openclaw.json by hand. Accept a Go-style duration string (digits with an
# optional s/m/h suffix). Empty/unset preserves the OpenClaw default.
_raw_heartbeat = (env.get("NEMOCLAW_AGENT_HEARTBEAT_EVERY") or "").strip()
if _raw_heartbeat and not re.match(r"^\d+(s|m|h)?$", _raw_heartbeat):
print(
f'[SECURITY] NEMOCLAW_AGENT_HEARTBEAT_EVERY must match ^\\d+(s|m|h)?$, '
f'got "{_raw_heartbeat}" — skipping override, preserving OpenClaw default',
file=sys.stderr,
)
_raw_heartbeat = ""
agent_heartbeat = _raw_heartbeat

model_specific_setups = _matching_model_specific_setups(
"openclaw",
{
Expand Down Expand Up @@ -534,6 +550,11 @@ def _placeholder(channel: str, env_key: str) -> str:
"defaults": {
"model": {"primary": primary_model_ref},
"timeoutSeconds": agent_timeout,
**(
{"heartbeat": {"every": agent_heartbeat}}
if agent_heartbeat
else {}
),
# NemoClaw sandboxes are provisioned non-interactively and the
# E2E CLI contract expects the first agent turn to answer the
# caller's prompt. OpenClaw 2026.4.24 seeds BOOTSTRAP.md by
Expand Down
10 changes: 10 additions & 0 deletions src/lib/onboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2469,6 +2469,16 @@ function patchStagedDockerfile(
`ARG NEMOCLAW_AGENT_TIMEOUT=${agentTimeout}`,
);
}
// NEMOCLAW_AGENT_HEARTBEAT_EVERY — override agents.defaults.heartbeat.every
// at build time. Accepts Go-style durations ("30m", "1h"); "0m" disables
// heartbeat. Ref: issue #2880
const agentHeartbeat = process.env.NEMOCLAW_AGENT_HEARTBEAT_EVERY;
if (agentHeartbeat && /^\d+(s|m|h)?$/.test(agentHeartbeat)) {
dockerfile = dockerfile.replace(
/^ARG NEMOCLAW_AGENT_HEARTBEAT_EVERY=.*$/m,
`ARG NEMOCLAW_AGENT_HEARTBEAT_EVERY=${agentHeartbeat}`,
);
}
// 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
27 changes: 27 additions & 0 deletions test/generate-openclaw-config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,33 @@ describe("generate-openclaw-config.py: config generation", () => {
expect(config.agents.defaults.timeoutSeconds).toBe(300);
});

it("omits heartbeat when NEMOCLAW_AGENT_HEARTBEAT_EVERY is unset", () => {
const config = runConfigScript();
expect(config.agents.defaults.heartbeat).toBeUndefined();
});

it("omits heartbeat when NEMOCLAW_AGENT_HEARTBEAT_EVERY is the empty string", () => {
// Docker promotes the unset ARG to an empty ENV value rather than dropping
// the variable, so the build path almost always sees "" rather than undefined.
const config = runConfigScript({ NEMOCLAW_AGENT_HEARTBEAT_EVERY: "" });
expect(config.agents.defaults.heartbeat).toBeUndefined();
});

it("propagates heartbeat cadence into agents.defaults.heartbeat.every", () => {
const config = runConfigScript({ NEMOCLAW_AGENT_HEARTBEAT_EVERY: "30m" });
expect(config.agents.defaults.heartbeat).toEqual({ every: "30m" });
});

it("disables heartbeat when set to 0m (NemoClaw#2880)", () => {
const config = runConfigScript({ NEMOCLAW_AGENT_HEARTBEAT_EVERY: "0m" });
expect(config.agents.defaults.heartbeat).toEqual({ every: "0m" });
});

it("rejects malformed heartbeat values and preserves OpenClaw default", () => {
const config = runConfigScript({ NEMOCLAW_AGENT_HEARTBEAT_EVERY: "5 minutes" });
expect(config.agents.defaults.heartbeat).toBeUndefined();
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved

it("disables OpenClaw first-run workspace bootstrap", () => {
const config = runConfigScript();
expect(config.agents.defaults.skipBootstrap).toBe(true);
Expand Down
Loading