Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
657f105
fix(status): retry a transient inference request refusal
gaveezy Sep 3, 2026
f80c299
docs(status): scope the first-attempt rule to an ordinary run
gaveezy Sep 3, 2026
e9214cc
refactor(inference): give the transient probe statuses one owner
gaveezy Sep 3, 2026
8f82b24
test(status): cover every transient status and the repeated route probe
gaveezy Sep 3, 2026
b37ec6f
refactor(inference): finish the transient status consolidation
gaveezy Sep 3, 2026
9abf64e
merge: bring main into the status transient retry branch
gaveezy Sep 3, 2026
82e140b
test(status): prove the retry believes the rechecked route
gaveezy Sep 3, 2026
7a63522
docs(status): correct the inference request count outside the command…
gaveezy Sep 3, 2026
16051db
Merge remote-tracking branch 'origin/main' into codex/pr-10956-followup
prekshivyas Sep 3, 2026
ff1042a
docs(status): clarify final inference failure
prekshivyas Sep 3, 2026
52eb6d0
Merge remote-tracking branch 'origin/main' into codex/pr-10956-followup
prekshivyas Sep 3, 2026
3571b14
fix(status): report inference probe retries
prekshivyas Sep 3, 2026
c9d4002
Merge remote-tracking branch 'origin/main' into codex/pr-10956-followup
prekshivyas Sep 3, 2026
151b69a
docs(status): clarify retry request accounting
prekshivyas Sep 3, 2026
6e5d3f8
Merge branch 'main' into fix/10709-status-transient-inference-503
prekshivyas Sep 3, 2026
2e595c5
Merge branch 'main' into fix/10709-status-transient-inference-503
prekshivyas Sep 3, 2026
72c2a8e
Merge branch 'main' into fix/10709-status-transient-inference-503
prekshivyas Sep 3, 2026
59bbb03
Merge branch 'main' into fix/10709-status-transient-inference-503
prekshivyas Sep 4, 2026
674a3b9
Merge branch 'main' into fix/10709-status-transient-inference-503
prekshivyas Sep 4, 2026
21f7c97
Merge remote-tracking branch 'origin/main' into codex/pr10956-finish
ericksoa Sep 4, 2026
1e68aac
Merge remote-tracking branch 'origin/main' into codex/pr10956-finish
ericksoa Sep 4, 2026
6674961
fix(ci): restore reviewed audit cleanup permissions
ericksoa Sep 4, 2026
5632324
Merge remote-tracking branch 'origin/main' into codex/pr10956-finish
ericksoa Sep 4, 2026
98151dd
chore(ci): use trusted base audit cleanup
ericksoa Sep 4, 2026
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
4 changes: 2 additions & 2 deletions docs/deployment/deploy-to-headless-server.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,7 @@ $$nemoclaw headless-agent status
$$nemoclaw headless-agent connect --probe-only
```

`$$nemoclaw headless-agent status` exits nonzero when the sandbox, gateway, local container, or authoritative inference route is not verified. Its main `Inference` line probes `https://inference.local/v1/models` from inside the sandbox, then sends one inference request over the same route when that probe reports the route reachable. The line reports `healthy` when the route served the request, `unauthorized` when the route rejected it with HTTP `401` or `403`, and `unhealthy` when the route returned HTTP `500` through `599`.
`$$nemoclaw headless-agent status` exits nonzero when the sandbox, gateway, local container, or authoritative inference route is not verified. Its main `Inference` line probes `https://inference.local/v1/models` from inside the sandbox, then sends an inference request over the same route when that probe reports the route reachable. It repeats both probes up to three total attempts when that request returns a transient gateway status; refer to the [CLI commands reference](../reference/commands) for the retried statuses and their token cost. The line reports `healthy` when the route served the request, `unauthorized` when the route rejected it with HTTP `401` or `403`, and `unhealthy` after a final failure or after all transient attempts fail.

<AgentOnly variant="openclaw,hermes">

Expand Down Expand Up @@ -441,7 +441,7 @@ Run `openshell sandbox list` and inspect the row for `headless-agent`. `NotReady

### Inference Returns HTTP 5xx

An HTTP status from `500` through `599` makes the authoritative `inference.local` route unhealthy. Check the configured provider and host egress, then run:
A final HTTP status from `500` through `599` makes the authoritative `inference.local` route unhealthy. HTTP `502`, `503`, and `504` report unhealthy only after all transient attempts fail. Check the configured provider and host egress, then run:

```bash
$$nemoclaw headless-agent doctor
Expand Down
2 changes: 1 addition & 1 deletion docs/inference/set-up-ollama.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -266,7 +266,7 @@ Scoped uninstall preserves the shared proxy state while a sibling gateway remain
All proxy endpoints require the token, including `GET /api/tags`.
The host-side proxy liveness check treats any HTTP response, including `401`, as evidence that the proxy answered.
The `status` and `doctor` route probe runs inside the sandbox and reports HTTP `200` through `499` as reachable, HTTP `500` through `599` as `unhealthy`, and transport failures or unavailable probes as `unreachable` or `not probed`.
`status` then sends one inference request over the reachable route and reports `healthy` when the route served it, or `unauthorized` when the proxy token is no longer accepted.
`status` then sends an inference request over the reachable route and reports `healthy` when the route served it, or `unauthorized` when the proxy token is no longer accepted.

If Ollama is already bound to a non-loopback address, onboarding restarts it on `127.0.0.1:11434` so the proxy becomes the only network path.

Expand Down
2 changes: 1 addition & 1 deletion docs/inference/verify-inference-route.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ $$nemoclaw <name> status
```

The `Inference` row first checks the sandbox's `inference.local` path.
When that route responds, `status` sends one inference request through the same path.
When that route responds, `status` sends an inference request through the same path.
When the live provider matches the recorded provider, `status` validates the result against the recorded API family, even when only the model differs.
When the live provider differs, `status` does not carry the recorded API family to the live provider.
The row reports `healthy` only when the route returns a structurally valid result for the selected API family.
Expand Down
4 changes: 2 additions & 2 deletions docs/reference/commands.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -1426,7 +1426,7 @@ For Portable Hermes, `status` reports `Portable lifecycle phase: pending`, `conf

For a `compatible-endpoint` route that uses `openai-completions`, the text output prints `Reasoning effort` as `low`, `medium`, `high`, or `endpoint-default`. The line is omitted for another provider or API family.

Pass `--json` to emit a structured per-sandbox report instead of the text renderer. The JSON output includes at least `schemaVersion`, `name`, `found`, `agent`, `agentDisplayName`, `agentRuntime`, `dcodeAutoApprovalMode`, `model`, `provider`, `recordedRoute`, `liveRoute`, `routeDrift`, `phase`, `gatewayState`, `inferenceHealth`, `rpcIssue`, `hostGpuDetected`, `sandboxGpuEnabled`, `sandboxGpuMode`, `sandboxGpuDevice`, `openshellDriver`, `openshellVersion`, `policies`, `policiesAvailable`, `failureLayer`, `terminalRuntimeHealth`, `servingProcessHealth`, and `dockerPaused`. `policies` is derived from the current OpenShell policy; NemoClaw does not persist a second preset list or baseline-exclusion ledger. `policiesAvailable` is `false` when that live policy cannot be read or parsed, distinguishing an unavailable result from a verified empty `policies` array; text status prints `Policies: unavailable` for the same state. The schema-version `1` `model` and `provider` fields keep their established live-route meaning when the gateway route is readable. Use `recordedRoute` for the sandbox's durable provider and model and `liveRoute` for the gateway-global route. When the live shared route differs, text output prints both routes and JSON output sets `routeDrift.live`, `routeDrift.recorded`, and `routeDrift.canConnect`. When `routeDrift.canConnect` is `false`, `connect` cannot safely restore the recorded route because provider-global identity differs or required route or gateway metadata is incomplete. Refer to [Use Shared Gateway Routes](../inference/manage-inference/use-shared-gateway-routes) for the route-sharing workflow. `openshellDriver` and `openshellVersion` are always strings (falling back to `"unknown"` when the registry has no value), so consumers can rely on `typeof` checks. `agent` is always a string and reports `openclaw` when the registry records no agent for the sandbox. `failureLayer` is `null` when no preflight failure was detected and otherwise one of `docker_unreachable`, `sandbox_container_stopped`, or `sandbox_dashboard_port_conflict`; when set, `inferenceHealth` is suppressed to `null` so automation does not see a stale remote-provider healthy status during a local outage. `inferenceHealth.ok` reports whether the inference route returned a structurally valid result for one request sent from inside the sandbox. The result must match Chat Completions, Responses, or Anthropic Messages for the selected route. An empty body, malformed JSON, provider-error envelope, or wrong response shape reports `unhealthy`, even with a 2xx status. The probe captures at most 64 KiB and does not include the response body in diagnostics. The route probe treats any final HTTP status from `200` through `499` as reachable, so a route with an invalidated provider credential answers HTTP `401` while the route is up. The request uses the live gateway route's provider and model, and falls back to the recorded values when the live route is unreadable. When the live provider matches the recorded provider, the request uses the sandbox's recorded API family, even when only the model differs. This includes `openai-responses`. When the live provider differs, NemoClaw does not carry the recorded API family to the live provider. An ordinary run sends one 16-token request through the stored provider credential, with a 30-second timeout, and consumes provider tokens on a hosted route. When the same `status` run recovers a managed gateway, it retries the route and inference request together up to three total attempts, with a two-second delay between failed attempts. Each attempt can consume another 16 tokens on a hosted route. When NemoClaw sends an inference request, `inferenceHealth.subprobes` reports the route probe result as the `route reachability` hop, so a failing verdict still shows that the route itself answered. `inferenceHealth.failureLabel` reports why the inference request failed:
Pass `--json` to emit a structured per-sandbox report instead of the text renderer. The JSON output includes at least `schemaVersion`, `name`, `found`, `agent`, `agentDisplayName`, `agentRuntime`, `dcodeAutoApprovalMode`, `model`, `provider`, `recordedRoute`, `liveRoute`, `routeDrift`, `phase`, `gatewayState`, `inferenceHealth`, `rpcIssue`, `hostGpuDetected`, `sandboxGpuEnabled`, `sandboxGpuMode`, `sandboxGpuDevice`, `openshellDriver`, `openshellVersion`, `policies`, `policiesAvailable`, `failureLayer`, `terminalRuntimeHealth`, `servingProcessHealth`, and `dockerPaused`. `policies` is derived from the current OpenShell policy; NemoClaw does not persist a second preset list or baseline-exclusion ledger. `policiesAvailable` is `false` when that live policy cannot be read or parsed, distinguishing an unavailable result from a verified empty `policies` array; text status prints `Policies: unavailable` for the same state. The schema-version `1` `model` and `provider` fields keep their established live-route meaning when the gateway route is readable. Use `recordedRoute` for the sandbox's durable provider and model and `liveRoute` for the gateway-global route. When the live shared route differs, text output prints both routes and JSON output sets `routeDrift.live`, `routeDrift.recorded`, and `routeDrift.canConnect`. When `routeDrift.canConnect` is `false`, `connect` cannot safely restore the recorded route because provider-global identity differs or required route or gateway metadata is incomplete. Refer to [Use Shared Gateway Routes](../inference/manage-inference/use-shared-gateway-routes) for the route-sharing workflow. `openshellDriver` and `openshellVersion` are always strings (falling back to `"unknown"` when the registry has no value), so consumers can rely on `typeof` checks. `agent` is always a string and reports `openclaw` when the registry records no agent for the sandbox. `failureLayer` is `null` when no preflight failure was detected and otherwise one of `docker_unreachable`, `sandbox_container_stopped`, or `sandbox_dashboard_port_conflict`; when set, `inferenceHealth` is suppressed to `null` so automation does not see a stale remote-provider healthy status during a local outage. `inferenceHealth.ok` reports whether the inference route returned a structurally valid result for one request sent from inside the sandbox. The result must match Chat Completions, Responses, or Anthropic Messages for the selected route. An empty body, malformed JSON, provider-error envelope, or wrong response shape reports `unhealthy`, even with a 2xx status. The probe captures at most 64 KiB and does not include the response body in diagnostics. The route probe treats any final HTTP status from `200` through `499` as reachable, so a route with an invalidated provider credential answers HTTP `401` while the route is up. The request uses the live gateway route's provider and model, and falls back to the recorded values when the live route is unreadable. When the live provider matches the recorded provider, the request uses the sandbox's recorded API family, even when only the model differs. This includes `openai-responses`. When the live provider differs, NemoClaw does not carry the recorded API family to the live provider. An ordinary run sends one 16-token request per attempt through the stored provider credential, with a 30-second timeout for each request, and consumes provider tokens on a hosted route. When the inference request returns HTTP `429`, `502`, `503`, or `504`, `status` retries the route and inference request together up to three total attempts, with a two-second delay between failed attempts, because those statuses are transient gateway and availability answers rather than evidence that the route is broken. Before each retry, it writes the failed probe boundary, an HTTP status when one is available, the next attempt, total attempts, and delay to stderr; `--json` keeps stdout machine-readable. On an ordinary run, every other failure is final on the first attempt with no delay: HTTP `401`, `403`, `404`, and `500`, an invalid 2xx response body, a request that returned no HTTP status, and a failing `/v1/models` route probe. When the same run recovers a managed gateway, `status` retries any failed route or inference probe on that schedule instead, while the restarted delivery chain settles. Each inference request can consume another 16 tokens on a hosted route. Each route probe has a 10-second timeout and each inference request has a 30-second timeout, so three complete probe pairs and two delays can take about 124 seconds. When NemoClaw sends an inference request, `inferenceHealth.subprobes` reports the route probe result as the `route reachability` hop, so a failing verdict still shows that the route itself answered. `inferenceHealth.failureLabel` reports why the inference request failed:

- `unauthorized` when the route rejected it with HTTP `401` or `403`.
- `unhealthy` when the route returned another failing HTTP status or an invalid 2xx response body.
Expand Down Expand Up @@ -1459,7 +1459,7 @@ $$nemoclaw my-assistant status --json
$$nemoclaw sandbox status my-assistant --json
```

The command probes `https://inference.local/v1/models` from inside the sandbox, and when that probe reports the route reachable it sends one inference request over the same route. That inference request is the authoritative inference health check, and both checks exercise the route that agent traffic uses. The main `Inference` line reports one of these states:
The command probes `https://inference.local/v1/models` from inside the sandbox, and when that probe reports the route reachable it sends an inference request over the same route. That inference request is the authoritative inference health check, and both checks exercise the route that agent traffic uses. The main `Inference` line reports one of these states:

| State | Meaning |
| --- | --- |
Expand Down
2 changes: 1 addition & 1 deletion docs/reference/troubleshooting.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -1889,7 +1889,7 @@ Verify that the inference provider endpoint is reachable from the host. Check th
$$nemoclaw <name> status
```

The main `Inference` line probes `https://inference.local/v1/models` from inside the sandbox and then sends one inference request over the same route, so it reflects the route the agent uses. If that line shows `unauthorized`, `unhealthy`, `unreachable`, or `not probed`, inspect the labeled diagnostic lines to identify the failing hop. An `unauthorized` line means the route answered but rejected the request, so refresh the provider credential rather than the route.
The main `Inference` line probes `https://inference.local/v1/models` from inside the sandbox and then sends an inference request over the same route, so it reflects the route the agent uses. When that request returns a transient gateway status, `status` repeats both probes up to three total attempts before it reports a failure. It prints the HTTP status, next attempt, and two-second delay to stderr before each retry; the configured timeout envelope for three complete probe pairs and two delays is about 124 seconds. If that line shows `unauthorized`, `unhealthy`, `unreachable`, or `not probed`, inspect the labeled diagnostic lines to identify the failing hop. An `unauthorized` line means the route answered but rejected the request, so refresh the provider credential rather than the route.

For local Ollama and local vLLM, `Inference (ollama backend)` or the corresponding local-backend line reports the host-side service separately. If a local-backend diagnostic fails, start the backend. For Local Ollama, current releases can also print `Inference (auth proxy)` when a proxy token is available. Docker Desktop on Windows Subsystem for Linux (WSL) reaches host loopback directly, so its auth-proxy diagnostic is non-authoritative. Native Docker Engine inside WSL is unqualified; enable Docker Desktop WSL integration, then rerun onboarding.

Expand Down
2 changes: 1 addition & 1 deletion docs/security/credential-rotation.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,7 @@ Plan for recreation downtime when automating messaging or web search rotation.

`$$nemoclaw credentials list` confirms that a provider exists, but it does not reveal or validate the stored value.
`$$nemoclaw status` performs host-side reachability checks without sending cloud API keys, so even an HTTP `401` or `403` can count as reachable there.
`$$nemoclaw <name> status` also sends one inference request through the stored provider credential and reports `unauthorized` when the route rejects it with HTTP `401` or `403`.
`$$nemoclaw <name> status` also sends an inference request through the stored provider credential and reports `unauthorized` when the route rejects it with HTTP `401` or `403`. It does not retry that rejection.
`$$nemoclaw inference get` reports the active route and does not authenticate a model request.

Complete a real request through the rotated integration before declaring the rotation successful.
Expand Down
57 changes: 57 additions & 0 deletions src/lib/actions/sandbox/inference-route-health.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
} from "./connect-inference-route-probe";
import {
buildSandboxInferenceRouteHealth,
isTransientInferenceInvocationFailure,
probeSandboxInferenceGatewayHealth,
type SandboxInferenceRouteHealth,
} from "./inference-route-health";
Expand Down Expand Up @@ -278,3 +279,59 @@ describe("buildSandboxInferenceRouteHealth (#10080)", () => {
expect(result.ok).toBe(true);
});
});

describe("transient inference invocation failures", () => {
it.each([429, 502, 503, 504])(
"treats HTTP %i as a transient inference request failure (#10709)",
(httpStatus) => {
expect(
isTransientInferenceInvocationFailure({
ok: false,
detail: `sandbox inference invocation probe returned HTTP ${httpStatus}`,
httpStatus,
}),
).toBe(true);
},
);

it.each([400, 401, 403, 404, 405, 500, 501])(
"treats HTTP %i as a settled inference request failure (#10709)",
(httpStatus) => {
expect(
isTransientInferenceInvocationFailure({
ok: false,
detail: `sandbox inference invocation probe returned HTTP ${httpStatus}`,
httpStatus,
}),
).toBe(false);
},
);

it("treats a served request as no failure at all (#10709)", () => {
expect(isTransientInferenceInvocationFailure({ ok: true })).toBe(false);
});

it("treats an invalid 2xx response body as a settled failure (#10709)", () => {
expect(
isTransientInferenceInvocationFailure({
ok: false,
detail: "sandbox inference invocation probe returned an invalid response body",
httpStatus: 200,
}),
).toBe(false);
});

it("treats a request that reached no HTTP status as a settled failure (#10709)", () => {
expect(
isTransientInferenceInvocationFailure({
ok: false,
detail: "sandbox inference invocation probe was unavailable",
httpStatus: null,
}),
).toBe(false);
});

it("reports no failure when no inference request was sent (#10709)", () => {
expect(isTransientInferenceInvocationFailure(null)).toBe(false);
});
});
Loading
Loading