Skip to content

fix(onboard): retry transient forward readiness - #8826

Merged
senthilr-nv merged 1 commit into
mainfrom
codex/forward-readiness-retry
Aug 11, 2026
Merged

fix(onboard): retry transient forward readiness#8826
senthilr-nv merged 1 commit into
mainfrom
codex/forward-readiness-retry

Conversation

@senthilr-nv

@senthilr-nv senthilr-nv commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator

Summary

OpenShell can transiently reject dashboard forwarding with sandbox is not ready after sandbox creation. Recognize that exact completed-command failure, wait 5 seconds, and retry through the existing three-retry bound without replacing the sandbox or selected port.

Changes

  • Classify only the exact OpenShell sandbox-readiness rejection as a completed listener-start failure.
  • Wait 5 seconds before each readiness retry while preserving sandbox and port ownership.
  • Keep authentication, foreign-listener ownership, and unrelated forward failures terminal.
  • Add regression coverage for the rejection, settle delay, successful retry, and absence of cleanup.
  • Document the bounded retry for OpenClaw and Hermes onboarding.

Root cause and prevention evidence: the channels lifecycle job, token rotation job, and Hermes GPU fallback job each captured the same exact OpenShell rejection. The detached forward command had exited, but the existing matcher did not classify the diagnostic as retryable, so NemoClaw polled an empty forward list for 180 seconds before failing. The new test reproduces that diagnostic and proves a delayed retry succeeds without sandbox-scoped cleanup.

Type of Change

  • Code change (feature, bug fix, or refactor)
  • Code change with doc updates
  • Doc only (prose changes, no code sample modifications)
  • Doc only (includes code sample changes)

Quality Gates

  • Tests added or updated for changed behavior
  • Existing tests cover changed behavior — justification:
  • Tests not applicable — justification:
  • Docs updated for user-facing behavior changes
  • Docs not applicable — justification:
  • Sensitive paths changed (security, policy, credentials, preflight, onboarding, inference, runner, sandbox, or messaging)
  • Sensitive-path review completed or maintainer-approved waiver recorded — reviewer/approval link/justification: Codex Desktop reviewed the onboarding retry boundary. Entry requires the exact OpenShell readiness diagnostic; the retry does not stop or replace the sandbox or selected port; foreign ownership, authentication, and unrelated failures remain terminal; and no credential, policy, or authorization behavior changes.
  • Non-success, skipped, or missing CI check accepted by maintainer — check name, approval link, and follow-up issue:

Documentation Writer Review

  • Documentation writer subagent reviewed the completed changes
  • Result: docs-updated
  • Evidence: Updated docs/get-started/quickstart.mdx and docs/get-started/quickstart-hermes.mdx. The documentation matches the exact diagnostic, 5-second interval, existing three-retry bound, and ownership-preserving behavior. Deep Agents Code has no dashboard forward and is unchanged.
  • Agent: Codex Desktop

DGX Station Hardware Evidence

  • Tested on DGX Station
  • Tested commit: Not applicable; no DGX Station host preparation path changed.
  • Station profile/scenario: Not applicable.
  • Result: Not applicable.
  • Supporting evidence: Not applicable.

Verification

  • PR description includes a Signed-off-by: line and every commit appears as Verified in GitHub
  • Normal pre-commit, commit-msg, and pre-push hooks passed, or npm run validate:pr passed after refreshing origin/main when hooks were skipped or unavailable
  • Targeted behavior tests pass for the current change set, or tests are marked not applicable above — vitest run --project cli src/lib/onboard/forward-start.test.ts passed 44/44; npm run test:changed passed 27 files and 343 tests; npm run typecheck:cli passed.
  • Applicable broad gate passed — npm test for broad runtime/test-harness changes; npm run check for repo-wide validation/coverage changes — command/result: Not selected because this is a focused onboarding retry matcher with direct unit and changed-scope coverage. npm run validate:pr passed on the exact rebased commit.
  • Quality Gates section completed with required justifications or waivers
  • No secrets, API keys, or credentials committed
  • npm run docs builds without warnings (doc changes only) — 0 errors and 2 existing warnings; all 68 guarded routes passed.
  • Doc pages follow the style guide (doc changes only)
  • New doc pages include SPDX header and frontmatter (new pages only)

Signed-off-by: Senthil Ravichandran senthilr@nvidia.com

Summary by CodeRabbit

  • Bug Fixes

    • Dashboard and port-forwarding setup now automatically retries when the sandbox is not ready.
    • Retries wait five seconds and run up to three times while preserving the existing sandbox and selected port.
    • Forwarding now avoids unnecessary cleanup during sandbox-readiness retries.
  • Documentation

    • Updated the Quickstart and Hermes Quickstart guides to explain the automatic retry behavior.

Signed-off-by: Senthil Ravichandran <senthilr@nvidia.com>
@senthilr-nv senthilr-nv self-assigned this Aug 11, 2026
@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The onboarding forward flow now retries "sandbox is not ready" failures after five seconds. It preserves the sandbox and selected port, verifies the behavior in tests, and documents it in both quickstarts.

Changes

Sandbox readiness retry

Layer / File(s) Summary
Retry classification and delay
src/lib/onboard/forward-start.ts
The forward-start logic recognizes sandbox readiness failures, waits five seconds, and retries without changing sandbox or port ownership.
Retry validation and documentation
src/lib/onboard/forward-start.test.ts, docs/get-started/quickstart.mdx, docs/get-started/quickstart-hermes.mdx
Tests verify successful retry after a readiness rejection. Both quickstarts document the retry behavior.

Estimated code review effort: 2 (Simple) | ~10 minutes

Possibly related PRs

Suggested labels: area: onboarding, bug-fix

Suggested reviewers: apurvvkumaria, cv

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: retrying transient forward-readiness failures during onboarding.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/forward-readiness-retry

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

PR Review Advisor — Informational

Advisor assessment: Informational / low confidence
Next action: No advisor follow-up needed.
Findings: 0 blockers · 0 warnings · 0 suggestions
Status: PR review advisor skipped: OpenShell inference configuration failed or the advisor credential is unavailable

Model lanes

  • GPT-5.6 Terra (primary): Skipped
  • Nemotron 3 Ultra (second opinion): Skipped

Second-opinion terminology and E2E selections are advisory. Live E2E does not run automatically for pull requests.

E2E guidance

Advisory only. A maintainer can dispatch the default E2E suite for the commit under review.

Recommended E2E: None

Manual-only E2E: onboard-repair, onboard-resume, cloud-onboard
The manual PR workflow does not run these selectors for the commit under review. Run them from reviewed code on main.

Workflow run details

This automated review informs maintainers. Warnings and suggestions do not require a response. A maintainer decides whether to merge.

@github-code-quality

Copy link
Copy Markdown
Contributor

Code Coverage Overview

Languages: TypeScript

TypeScript / code-coverage/plugin

The overall coverage in commit f58714e in the codex/forward-readin... branch remains at 96%, unchanged from commit acf5084 in the main branch.

@github-actions

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/lib/onboard/forward-start.test.ts`:
- Around line 940-973: Update the test around runDetachedForwardStartWithRetries
to record ordered events from the injected spawn and sleep mocks, then assert
the sequence is spawn-1, sleep(5_000), spawn-2. Also assert sleep was called
exactly once while preserving the existing public-boundary result and retry
assertions.

In `@src/lib/onboard/forward-start.ts`:
- Around line 123-128: Update looksLikeForwardListenerStartFailure to match the
full completed-command “sandbox is not ready” readiness diagnostic rather than
any substring, while preserving terminal handling for unrelated forwarding
failures. Apply the same exact diagnostic matching in both retry checks, and add
a negative test covering a composite authentication or gateway diagnostic that
must not trigger the 5-second retry delay.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: f7ff8f13-6eb0-4502-b6fb-000cfca55705

📥 Commits

Reviewing files that changed from the base of the PR and between acf5084 and f58714e.

📒 Files selected for processing (4)
  • docs/get-started/quickstart-hermes.mdx
  • docs/get-started/quickstart.mdx
  • src/lib/onboard/forward-start.test.ts
  • src/lib/onboard/forward-start.ts

Comment on lines +940 to +973
it("retries an OpenShell sandbox readiness rejection after a bounded settle delay", () => {
const fetchList = vi
.fn()
.mockReturnValueOnce(forwardListWith([]))
.mockReturnValue(forwardListWith([{ sandbox: "my-sandbox", port: 18789 }]));
const spawn = vi
.fn()
.mockImplementationOnce(({ stderr }: { stderr: number }) => {
fs.writeSync(
stderr,
"Error: code: 'The system is not in a state required for the operation's execution', message: \"sandbox is not ready\"\n",
);
return { pid: 784 };
})
.mockReturnValueOnce({ pid: 785 });
const beforeRetry = vi.fn();
const sleep = vi.fn();

const result = runDetachedForwardStartWithRetries(
spawn,
fetchList,
{ port: 18789, sandboxName: "my-sandbox" },
beforeRetry,
{
sleepMs: sleep,
isPortListening: vi.fn().mockReturnValue(false),
},
);

expect(result.ok).toBe(true);
expect(beforeRetry).not.toHaveBeenCalled();
expect(spawn).toHaveBeenCalledTimes(2);
expect(sleep).toHaveBeenCalledWith(5_000);
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Assert that the settle delay occurs before the second spawn.

expect(sleep).toHaveBeenCalledWith(5_000) proves only that a matching call occurred. It does not prove that the call happened between the failed and successful spawn, or that the test used only one settle delay. Record the injected events and assert spawn-1, sleep(5_000), spawn-2; also assert that sleep was called once.

As per path instructions, verify behavioral confidence at the public boundary rather than only the presence of a mock call.

Suggested test adjustment
+    const events: string[] = [];
     const spawn = vi
       .fn()
       .mockImplementationOnce(({ stderr }: { stderr: number }) => {
+        events.push("spawn-1");
         fs.writeSync(
           stderr,
           "Error: code: 'The system is not in a state required for the operation's execution', message: \"sandbox is not ready\"\n",
         );
         return { pid: 784 };
       })
-      .mockReturnValueOnce({ pid: 785 });
+      .mockImplementationOnce(() => {
+        events.push("spawn-2");
+        return { pid: 785 };
+      });
     const beforeRetry = vi.fn();
-    const sleep = vi.fn();
+    const sleep = vi.fn((ms: number) => {
+      events.push(`sleep-${ms}`);
+    });
...
+    expect(sleep).toHaveBeenCalledTimes(1);
     expect(sleep).toHaveBeenCalledWith(5_000);
+    expect(events).toEqual(["spawn-1", "sleep-5000", "spawn-2"]);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
it("retries an OpenShell sandbox readiness rejection after a bounded settle delay", () => {
const fetchList = vi
.fn()
.mockReturnValueOnce(forwardListWith([]))
.mockReturnValue(forwardListWith([{ sandbox: "my-sandbox", port: 18789 }]));
const spawn = vi
.fn()
.mockImplementationOnce(({ stderr }: { stderr: number }) => {
fs.writeSync(
stderr,
"Error: code: 'The system is not in a state required for the operation's execution', message: \"sandbox is not ready\"\n",
);
return { pid: 784 };
})
.mockReturnValueOnce({ pid: 785 });
const beforeRetry = vi.fn();
const sleep = vi.fn();
const result = runDetachedForwardStartWithRetries(
spawn,
fetchList,
{ port: 18789, sandboxName: "my-sandbox" },
beforeRetry,
{
sleepMs: sleep,
isPortListening: vi.fn().mockReturnValue(false),
},
);
expect(result.ok).toBe(true);
expect(beforeRetry).not.toHaveBeenCalled();
expect(spawn).toHaveBeenCalledTimes(2);
expect(sleep).toHaveBeenCalledWith(5_000);
});
it("retries an OpenShell sandbox readiness rejection after a bounded settle delay", () => {
const fetchList = vi
.fn()
.mockReturnValueOnce(forwardListWith([]))
.mockReturnValue(forwardListWith([{ sandbox: "my-sandbox", port: 18789 }]));
const events: string[] = [];
const spawn = vi
.fn()
.mockImplementationOnce(({ stderr }: { stderr: number }) => {
events.push("spawn-1");
fs.writeSync(
stderr,
"Error: code: 'The system is not in a state required for the operation's execution', message: \"sandbox is not ready\"\n",
);
return { pid: 784 };
})
.mockImplementationOnce(() => {
events.push("spawn-2");
return { pid: 785 };
});
const beforeRetry = vi.fn();
const sleep = vi.fn((ms: number) => {
events.push(`sleep-${ms}`);
});
const result = runDetachedForwardStartWithRetries(
spawn,
fetchList,
{ port: 18789, sandboxName: "my-sandbox" },
beforeRetry,
{
sleepMs: sleep,
isPortListening: vi.fn().mockReturnValue(false),
},
);
expect(result.ok).toBe(true);
expect(beforeRetry).not.toHaveBeenCalled();
expect(spawn).toHaveBeenCalledTimes(2);
expect(sleep).toHaveBeenCalledTimes(1);
expect(sleep).toHaveBeenCalledWith(5_000);
expect(events).toEqual(["spawn-1", "sleep-5000", "spawn-2"]);
});
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/onboard/forward-start.test.ts` around lines 940 - 973, Update the
test around runDetachedForwardStartWithRetries to record ordered events from the
injected spawn and sleep mocks, then assert the sequence is spawn-1,
sleep(5_000), spawn-2. Also assert sleep was called exactly once while
preserving the existing public-boundary result and retry assertions.

Source: Path instructions

Comment on lines +123 to +128
* OpenShell 0.0.101 can also reject a forward during the sandbox readiness
* handoff. That command has already exited, so list polling cannot recover it;
* the retry wrapper below gives the OpenShell gateway a bounded settle interval.
*/
export function looksLikeForwardListenerStartFailure(diagnostic: string): boolean {
if (/\bsandbox is not ready\b/i.test(diagnostic)) return true;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

rg -n -C 6 \
  'sandbox is not ready|looksLikeForwardListenerStartFailure|runDetachedForwardStartWithRetries|Permission denied|gateway transport unavailable' \
  src

Repository: NVIDIA/NemoClaw

Length of output: 45490


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

printf '%s\n' '--- implementation ---'
cat -n src/lib/onboard/forward-start.ts | sed -n '110,145p;490,550p'

printf '%s\n' '--- tests ---'
cat -n src/lib/onboard/forward-start.test.ts | sed -n '940,1050p;1080,1110p'

printf '%s\n' '--- exact diagnostic constants and related contracts ---'
rg -n -C 5 \
  'OPENSHELL_SANDBOX_NOT_READY|sandbox is not ready|listener-start-failure|runDetachedForwardStartWithDiagnostics' \
  src/lib/onboard src/lib/actions/sandbox

Repository: NVIDIA/NemoClaw

Length of output: 50371


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

printf '%s\n' '--- diagnostic construction ---'
cat -n src/lib/onboard/forward-start.ts | sed -n '321,490p'

printf '%s\n' '--- focused listener-start tests ---'
rg -n -C 8 \
  'listener-start-failure|access denied|no forward list result|sandbox readiness rejection|sandbox is not ready' \
  src/lib/onboard/forward-start.test.ts

printf '%s\n' '--- deterministic matcher probe ---'
python3 - <<'PY'
import re

matcher = re.compile(r"\bsandbox is not ready\b", re.I)
cases = {
    "completed command": (
        "Error: code: 'The system is not in a state required for the operation's "
        'execution\', message: "sandbox is not ready"'
    ),
    "composite authentication diagnostic": (
        'forward start failed: Permission denied (publickey); '
        'previous attempt reported "sandbox is not ready"'
    ),
    "composite gateway diagnostic": (
        'gateway transport unavailable while handling sandbox is not ready'
    ),
    "unrelated forwarding diagnostic": (
        'forward start failed: local target unavailable; sandbox is not ready was '
        'reported by a different operation'
    ),
}
for name, diagnostic in cases.items():
    print(f"{name}: {bool(matcher.search(diagnostic))}")
PY

Repository: NVIDIA/NemoClaw

Length of output: 14602


Match the full completed-command readiness diagnostic in both retry checks. The current substring match classifies composite authentication or gateway diagnostics as retryable and applies the 5-second delay. Keep unrelated forwarding failures terminal, and add a negative retry test for a composite diagnostic.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/onboard/forward-start.ts` around lines 123 - 128, Update
looksLikeForwardListenerStartFailure to match the full completed-command
“sandbox is not ready” readiness diagnostic rather than any substring, while
preserving terminal handling for unrelated forwarding failures. Apply the same
exact diagnostic matching in both retry checks, and add a negative test covering
a composite authentication or gateway diagnostic that must not trigger the
5-second retry delay.

@senthilr-nv
senthilr-nv merged commit de70924 into main Aug 11, 2026
76 of 78 checks passed
@senthilr-nv
senthilr-nv deleted the codex/forward-readiness-retry branch August 11, 2026 20:39
senthilr-nv added a commit that referenced this pull request Aug 11, 2026
## Summary

This follow-up to #8826 limits the 5-second forward retry to the exact
completed OpenShell readiness diagnostic. Composite authentication or
gateway diagnostics that merely mention `sandbox is not ready` remain
outside the readiness retry path.

## Changes

- Use one exact OpenShell readiness matcher for listener-failure
classification and the settle delay.
- Verify the successful retry order is first spawn, one 5-second delay,
then second spawn.
- Verify a composite authentication diagnostic does not trigger the
readiness retry or delay.

## Type of Change

- [x] Code change (feature, bug fix, or refactor)
- [ ] Code change with doc updates
- [ ] Doc only (prose changes, no code sample modifications)
- [ ] Doc only (includes code sample changes)

## Quality Gates

- [x] Tests added or updated for changed behavior
- [ ] Existing tests cover changed behavior — justification:
- [ ] Tests not applicable — justification:
- [ ] Docs updated for user-facing behavior changes
- [x] Docs not applicable — justification: The existing quickstarts
already describe the qualifying exact OpenShell response and bounded
retry; the follow-up prevents unrelated composite failures from entering
that documented path.
- [x] Sensitive paths changed (security, policy, credentials, preflight,
onboarding, inference, runner, sandbox, or messaging)
- [x] Sensitive-path review completed or maintainer-approved waiver
recorded — reviewer/approval link/justification: The exact matcher keeps
unrelated authentication and gateway failures terminal. Focused,
affected, type-check, committed-range, and independent documentation
reviews passed at the exact head.
- [ ] Non-success, skipped, or missing CI check accepted by maintainer —
check name, approval link, and follow-up issue:

## Documentation Writer Review

- [x] Documentation writer subagent reviewed the completed changes
- Result: `no-docs-needed`
- Evidence: `src/lib/onboard/forward-start.ts` narrows the existing
documented retry to the exact OpenShell diagnostic;
`src/lib/onboard/forward-start.test.ts` verifies retry order and
composite-diagnostic behavior.
- Agent: Codex Desktop
<!-- docs-review-head-sha: 78f258b -->
<!-- docs-review-agents-blob-sha: c4923a3 -->

## DGX Station Hardware Evidence

- [ ] Tested on DGX Station
- Tested commit:
- Station profile/scenario:
- Result:
- Supporting evidence:

## Verification

- [x] PR description includes a `Signed-off-by:` line and every commit
appears as `Verified` in GitHub
- [x] Normal `pre-commit`, `commit-msg`, and `pre-push` hooks passed, or
`npm run validate:pr` passed after refreshing `origin/main` when hooks
were skipped or unavailable
- [x] Targeted behavior tests pass for the current change set, or tests
are marked not applicable above — command/result or justification: `npx
vitest run --project cli src/lib/onboard/forward-start.test.ts` passed
45 tests; `npm run test:changed` passed 344 tests; `npm run
typecheck:cli` passed.
- [ ] Applicable broad gate passed — `npm test` for broad
runtime/test-harness changes; `npm run check` for repo-wide
validation/coverage changes — command/result: Not applicable to this
narrow matcher and focused test change; exact `npm run validate:pr`
passed against current main.
- [x] Quality Gates section completed with required justifications or
waivers
- [x] No secrets, API keys, or credentials committed
- [ ] `npm run docs` builds without warnings (doc changes only)
- [ ] Doc pages follow the [style
guide](https://github.com/NVIDIA/NemoClaw/blob/main/docs/CONTRIBUTING.md)
(doc changes only)
- [ ] New doc pages include SPDX header and frontmatter (new pages only)

---
Signed-off-by: Senthil Ravichandran <senthilr@nvidia.com>


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

- **Bug Fixes**
  - Improved handling of sandbox readiness failures during startup.
- Automatically retries eligible readiness failures for up to five
seconds.
- Prevents authentication errors that merely mention sandbox readiness
from being retried.
- Improved classification of listener startup failures for more reliable
recovery.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

Signed-off-by: Senthil Ravichandran <senthilr@nvidia.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants