Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
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
31 changes: 9 additions & 22 deletions src/lib/onboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8901,11 +8901,7 @@ const onboardRuntimeBoundary = new OnboardRuntimeBoundary({
toSessionUpdates,
maybeForceE2eStepFailure,
});
const startRecordedStep = onboardRuntimeBoundary.startRecordedStep.bind(onboardRuntimeBoundary);
const recordStepComplete = onboardRuntimeBoundary.recordStepComplete.bind(onboardRuntimeBoundary);
const recordStepSkipped = onboardRuntimeBoundary.recordStepSkipped.bind(onboardRuntimeBoundary);
const recordStepFailed = onboardRuntimeBoundary.recordStepFailed.bind(onboardRuntimeBoundary);
const recordSessionComplete = onboardRuntimeBoundary.recordSessionComplete.bind(onboardRuntimeBoundary);
const onboardRuntimeRecorders = onboardRuntimeBoundary.recorders();

const ONBOARD_STEP_INDEX: Record<string, { number: number; title: string }> = {
preflight: { number: 1, title: "Preflight checks" },
Expand Down Expand Up @@ -9293,8 +9289,7 @@ async function onboard(opts: OnboardOptions = {}): Promise<void> {
resolveSandboxGpuConfig,
validateSandboxGpuPreflight,
skippedStepMessage,
startRecordedStep,
recordStepComplete,
...onboardRuntimeRecorders,
updateSession: onboardSession.updateSession,
},
});
Expand Down Expand Up @@ -9361,10 +9356,9 @@ async function onboard(opts: OnboardOptions = {}): Promise<void> {
retireLegacyGatewayForDockerDriverUpgrade,
destroyGatewayRuntimeForGpuReuse: () => destroyGateway(() => undefined, () => false),
skippedStepMessage,
...onboardRuntimeRecorders,
note,
startRecordedStep,
startGateway,
recordStepComplete,
exitProcess: (code) => process.exit(code),
},
});
Expand Down Expand Up @@ -9412,8 +9406,7 @@ async function onboard(opts: OnboardOptions = {}): Promise<void> {
normalizeHermesAuthMethod,
setupNim,
setupInference,
startRecordedStep,
recordStepComplete,
...onboardRuntimeRecorders,
toSessionUpdates: (updates) => toSessionUpdates(updates as Parameters<typeof toSessionUpdates>[0]),
skippedStepMessage,
ensureResumeProviderReady,
Expand Down Expand Up @@ -9496,7 +9489,7 @@ async function onboard(opts: OnboardOptions = {}): Promise<void> {
ensureValidatedBraveSearchCredential,
isBackToSelection,
configureWebSearch,
startRecordedStep,
...onboardRuntimeRecorders,
getRecordedMessagingChannelsForResume,
getSandboxMessagingChannels: (name) => registry.getSandbox(name)?.messagingChannels,
setupMessagingChannels,
Expand All @@ -9508,7 +9501,6 @@ async function onboard(opts: OnboardOptions = {}): Promise<void> {
updateSandboxRegistry: (name, updates) => registry.updateSandbox(name, updates),
setDefaultSandbox: registry.setDefault,
getSandboxAgentRegistryFields,
recordStepComplete,
toSessionUpdates: (updates) => toSessionUpdates(updates as Parameters<typeof toSessionUpdates>[0]),
skippedStepMessage,
error: (message) => console.error(message),
Expand Down Expand Up @@ -9537,19 +9529,15 @@ async function onboard(opts: OnboardOptions = {}): Promise<void> {
runCaptureOpenshell,
openshellShellCommand,
openshellBinary: getOpenshellBinary(),
startRecordedStep,
recordStepComplete,
recordStepFailed,
...onboardRuntimeRecorders,
skippedStepMessage,
}),
ensureAgentDashboardForward,
recordStepSkipped,
...onboardRuntimeRecorders,
isOpenclawReady,
skippedStepMessage,
startRecordedStep,
setupOpenclaw,
syncNemoClawConfigInSandbox,
recordStepComplete,
toSessionUpdates: (updates) => toSessionUpdates(updates as Parameters<typeof toSessionUpdates>[0]),
},
});
Expand Down Expand Up @@ -9581,10 +9569,9 @@ async function onboard(opts: OnboardOptions = {}): Promise<void> {
preparePolicyPresetResumeSelection({ policies }, name, options),
arePolicyPresetsApplied,
skippedStepMessage,
startRecordedStep,
...onboardRuntimeRecorders,
setupPoliciesWithSelection,
updateSession: onboardSession.updateSession,
recordStepComplete,
toSessionUpdates: (updates) => toSessionUpdates(updates as Parameters<typeof toSessionUpdates>[0]),
},
});
Expand All @@ -9602,7 +9589,7 @@ async function onboard(opts: OnboardOptions = {}): Promise<void> {
migratedLegacyKeys,
deps: {
ensureAgentDashboardForward,
recordSessionComplete,
...onboardRuntimeRecorders,
toSessionUpdates: (updates) => toSessionUpdates(updates as Parameters<typeof toSessionUpdates>[0]),
removeLegacyCredentialsFile,
cleanupStaleHostFiles,
Expand Down
6 changes: 6 additions & 0 deletions src/lib/onboard/machine/handlers/agent-setup.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ function createDeps(overrides: Partial<AgentSetupStateOptions<Agent>["deps"]> =
}),
openclawReady: vi.fn(() => false),
skippedMessage: vi.fn(),
recordSkip: vi.fn(async () => createSession()),
startStep: vi.fn(async () => undefined),
setupOpenclaw: vi.fn(async () => undefined),
syncConfig: vi.fn(),
Expand All @@ -38,6 +39,7 @@ function createDeps(overrides: Partial<AgentSetupStateOptions<Agent>["deps"]> =
recordStepSkipped: calls.skipped,
isOpenclawReady: calls.openclawReady,
skippedStepMessage: calls.skippedMessage,
recordStateSkipped: calls.recordSkip,
startRecordedStep: calls.startStep,
setupOpenclaw: calls.setupOpenclaw,
syncNemoClawConfigInSandbox: calls.syncConfig,
Expand Down Expand Up @@ -94,6 +96,10 @@ describe("handleAgentSetupState", () => {
const result = await handleAgentSetupState({ ...baseOptions(deps), resume: true });

expect(calls.skippedMessage).toHaveBeenCalledWith("openclaw", "my-assistant");
expect(calls.recordSkip).toHaveBeenCalledWith("openclaw", {
reason: "resume",
sandboxName: "my-assistant",
});
expect(calls.startStep).not.toHaveBeenCalled();
expect(calls.setupOpenclaw).not.toHaveBeenCalled();
expect(calls.syncConfig).toHaveBeenCalledWith("my-assistant", "provider", "model");
Expand Down
4 changes: 3 additions & 1 deletion src/lib/onboard/machine/handlers/agent-setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
recordStepSkipped(stepName: string): Promise<Session>;
isOpenclawReady(sandboxName: string): boolean;
skippedStepMessage(stepName: string, detail?: string | null): void;
recordStateSkipped(state: "openclaw", metadata?: Record<string, unknown> | null): Promise<Session>;
startRecordedStep(
stepName: string,
updates: { sandboxName: string; provider: string; model: string },
Expand Down Expand Up @@ -72,10 +73,11 @@
if (resumeOpenclaw) {
deps.skippedStepMessage("openclaw", sandboxName);
deps.syncNemoClawConfigInSandbox(sandboxName, provider, model);
await deps.recordStepComplete(
await deps.recordStateSkipped("openclaw", { reason: "resume", sandboxName });
session = await deps.recordStepComplete(
"openclaw",
deps.toSessionUpdates({ sandboxName, provider, model, hermesAuthMethod, hermesToolGateways }),
);

Check warning

Code scanning / CodeQL

Useless assignment to local variable Warning

The value assigned to session here is unused.
} else {
await deps.startRecordedStep("openclaw", { sandboxName, provider, model });
await deps.setupOpenclaw(sandboxName, model, provider);
Expand Down
10 changes: 10 additions & 0 deletions src/lib/onboard/machine/handlers/gateway.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ function createDeps(overrides: Partial<GatewayStateOptions<Gpu>["deps"]> = {}) {
retireLegacy: vi.fn(),
destroyGpuRuntime: vi.fn(() => true),
skipped: vi.fn(),
recordSkip: vi.fn(async () => createSession()),
note: vi.fn(),
startStep: vi.fn(async () => undefined),
startGateway: vi.fn(async () => undefined),
Expand All @@ -52,6 +53,7 @@ function createDeps(overrides: Partial<GatewayStateOptions<Gpu>["deps"]> = {}) {
retireLegacyGatewayForDockerDriverUpgrade: calls.retireLegacy,
destroyGatewayRuntimeForGpuReuse: calls.destroyGpuRuntime,
skippedStepMessage: calls.skipped,
recordStateSkipped: calls.recordSkip,
note: calls.note,
startRecordedStep: calls.startStep,
startGateway: calls.startGateway,
Expand Down Expand Up @@ -99,6 +101,10 @@ describe("handleGatewayState", () => {
await handleGatewayState(baseOptions(deps, "healthy"));

expect(calls.skipped).toHaveBeenCalledWith("gateway", "running", "reuse");
expect(calls.recordSkip).toHaveBeenCalledWith("gateway", {
reason: "reuse",
reuseState: "healthy",
});
expect(calls.note).toHaveBeenCalledWith(" Reusing healthy NemoClaw gateway.");
expect(calls.startGateway).not.toHaveBeenCalled();
expect(calls.complete).toHaveBeenCalledWith("gateway");
Expand All @@ -112,6 +118,10 @@ describe("handleGatewayState", () => {
await handleGatewayState({ ...baseOptions(deps, "healthy", session), resume: true });

expect(calls.skipped).toHaveBeenCalledWith("gateway", "running");
expect(calls.recordSkip).toHaveBeenCalledWith("gateway", {
reason: "resume",
reuseState: "healthy",
});
expect(calls.startGateway).not.toHaveBeenCalled();
});

Expand Down
3 changes: 3 additions & 0 deletions src/lib/onboard/machine/handlers/gateway.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ export interface GatewayStateOptions<Gpu> {
detail?: string | null,
reason?: "resume" | "reuse",
): void;
recordStateSkipped(state: "gateway", metadata?: Record<string, unknown> | null): Promise<Session>;
note(message: string): void;
startRecordedStep(stepName: string): Promise<void>;
startGateway(gpu: Gpu, options: { gpuPassthrough: boolean }): Promise<void>;
Expand Down Expand Up @@ -150,9 +151,11 @@ export async function handleGatewayState<Gpu>({
const resumeGateway = resume && session?.steps?.gateway?.status === "complete" && canReuseHealthyGateway;
if (resumeGateway) {
deps.skippedStepMessage("gateway", "running");
await deps.recordStateSkipped("gateway", { reason: "resume", reuseState: gatewayReuseState });
session = await deps.recordStepComplete("gateway");
} else if (!resume && canReuseHealthyGateway) {
deps.skippedStepMessage("gateway", "running", "reuse");
await deps.recordStateSkipped("gateway", { reason: "reuse", reuseState: gatewayReuseState });
deps.note(" Reusing healthy NemoClaw gateway.");
session = await deps.recordStepComplete("gateway");
} else {
Expand Down
6 changes: 6 additions & 0 deletions src/lib/onboard/machine/handlers/policies.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ function createDeps(overrides: Partial<PoliciesStateOptions<Agent, WebSearchConf
),
appliedCheck: vi.fn(() => false),
skipped: vi.fn(),
recordSkip: vi.fn(async () => session),
startStep: vi.fn(async () => undefined),
setupPolicies: vi.fn(async () => ["npm"]),
updateSession: vi.fn((mutator: (value: Session) => Session | void) => {
Expand All @@ -52,6 +53,7 @@ function createDeps(overrides: Partial<PoliciesStateOptions<Agent, WebSearchConf
preparePolicyPresetResumeSelection: calls.prepareResume,
arePolicyPresetsApplied: calls.appliedCheck,
skippedStepMessage: calls.skipped,
recordStateSkipped: calls.recordSkip,
startRecordedStep: calls.startStep,
setupPoliciesWithSelection: calls.setupPolicies,
updateSession: calls.updateSession,
Expand Down Expand Up @@ -146,6 +148,10 @@ describe("handlePoliciesState", () => {
const result = await handlePoliciesState({ ...baseOptions(deps), resume: true });

expect(calls.skipped).toHaveBeenCalledWith("policies", "npm");
expect(calls.recordSkip).toHaveBeenCalledWith("policies", {
reason: "resume",
policyPresets: ["npm"],
});
expect(calls.setupPolicies).not.toHaveBeenCalled();
expect(calls.complete).toHaveBeenCalledWith(
"policies",
Expand Down
5 changes: 5 additions & 0 deletions src/lib/onboard/machine/handlers/policies.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ export interface PoliciesStateOptions<Agent, WebSearchConfig> {
): PolicyResumeSelection;
arePolicyPresetsApplied(sandboxName: string, selectedPresets: string[]): boolean;
skippedStepMessage(stepName: string, detail?: string | null): void;
recordStateSkipped(state: "policies", metadata?: Record<string, unknown> | null): Promise<Session>;
startRecordedStep(
stepName: string,
updates: { sandboxName: string; provider: string; model: string; policyPresets: string[] },
Expand Down Expand Up @@ -148,6 +149,10 @@ export async function handlePoliciesState<Agent, WebSearchConfig>({
let session: Session | null;
if (resumePolicies) {
deps.skippedStepMessage("policies", recordedPolicyPresetsForSupport.join(", "));
await deps.recordStateSkipped("policies", {
reason: "resume",
policyPresets: recordedPolicyPresetsForSupport,
});
session = await deps.recordStepComplete(
"policies",
deps.toSessionUpdates({
Expand Down
5 changes: 5 additions & 0 deletions src/lib/onboard/machine/handlers/preflight.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ function createDeps(overrides: Partial<PreflightStateOptions<Gpu, SandboxEntry,
}),
validateSandboxGpuPreflight: vi.fn(),
skippedStepMessage: vi.fn(),
recordStateSkipped: vi.fn(async () => session),
startRecordedStep: vi.fn(async () => undefined),
recordStepComplete: vi.fn(async () => session),
updateSession: vi.fn((mutator: (value: Session) => Session | void) => {
Expand Down Expand Up @@ -125,6 +126,10 @@ describe("handlePreflightState", () => {
});

expect(harness.deps.skippedStepMessage).toHaveBeenCalledWith("preflight", "cached");
expect(harness.deps.recordStateSkipped).toHaveBeenCalledWith("preflight", {
reason: "resume",
validation: "gpu-cdi",
});
expect(harness.deps.detectGpu).toHaveBeenCalledOnce();
expect(harness.deps.runPreflight).not.toHaveBeenCalled();
expect(harness.deps.startRecordedStep).not.toHaveBeenCalled();
Expand Down
2 changes: 2 additions & 0 deletions src/lib/onboard/machine/handlers/preflight.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ export interface PreflightStateOptions<
): Config;
validateSandboxGpuPreflight(config: Config): void;
skippedStepMessage(stepName: string, detail?: string | null): void;
recordStateSkipped(state: "preflight", metadata?: Record<string, unknown> | null): Promise<Session>;
startRecordedStep(stepName: string): Promise<void>;
recordStepComplete(stepName: string): Promise<Session>;
updateSession(mutator: (session: Session) => Session | void): Session;
Expand Down Expand Up @@ -111,6 +112,7 @@ export async function handlePreflightState<
let gpu: Gpu;
if (resumePreflight) {
deps.skippedStepMessage("preflight", "cached");
await deps.recordStateSkipped("preflight", { reason: "resume", validation: "gpu-cdi" });
gpu = deps.detectGpu();
const resumeSandboxGpuConfig = deps.resolveSandboxGpuConfig(gpu, {
flag: effectiveSandboxGpuFlag,
Expand Down
56 changes: 56 additions & 0 deletions src/lib/onboard/machine/handlers/provider-inference.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ function createDeps(overrides: Partial<ProviderInferenceStateOptions<Gpu, Agent,
forceInferenceSetup: false,
credentialEnv: credentialEnv ?? null,
})),
recordSkip: vi.fn(async () => createSession()),
repairEvent: vi.fn(async () => createSession()),
hydrate: vi.fn(),
repair: vi.fn(),
routeReady: vi.fn(() => false),
Expand All @@ -61,6 +63,8 @@ function createDeps(overrides: Partial<ProviderInferenceStateOptions<Gpu, Agent,
toSessionUpdates: (updates: Record<string, unknown>) => updates as SessionUpdates,
skippedStepMessage: calls.skipped,
ensureResumeProviderReady: calls.recoverProvider,
recordStateSkipped: calls.recordSkip,
recordRepairEvent: calls.repairEvent,
hydrateCredentialEnv: calls.hydrate,
repairLocalInferenceSystemdOverrideOrExit: calls.repair,
isNonInteractive: () => true,
Expand Down Expand Up @@ -218,12 +222,64 @@ describe("handleProviderInferenceState", () => {
expect(calls.setupInference).not.toHaveBeenCalled();
expect(calls.recoverProvider).toHaveBeenCalledWith("ollama-local", null);
expect(calls.skipped).toHaveBeenCalledWith("provider_selection", "ollama-local / llama3.1");
expect(calls.recordSkip).toHaveBeenCalledWith("provider_selection", {
reason: "resume",
provider: "ollama-local",
model: "llama3.1",
});
expect(calls.hydrate).toHaveBeenCalledWith(null);
expect(calls.repairEvent).toHaveBeenCalledWith("state.repair.started", {
state: "provider_selection",
metadata: { repair: "ollama-systemd-loopback" },
});
expect(calls.repair).toHaveBeenCalledWith("ollama-local", deps.isNonInteractive);
expect(calls.repairEvent).toHaveBeenCalledWith("state.repair.completed", {
state: "provider_selection",
metadata: { repair: "ollama-systemd-loopback" },
});
expect(calls.skipped).toHaveBeenCalledWith("inference", "ollama-local / llama3.1");
expect(calls.recordSkip).toHaveBeenCalledWith("inference", {
reason: "resume",
provider: "ollama-local",
model: "llama3.1",
});
expect(result).toMatchObject({ provider: "ollama-local", model: "llama3.1" });
});

it("records failed Ollama repair events before propagating resume repair errors", async () => {
const session = createSession({
provider: "ollama-local",
model: "llama3.1",
credentialEnv: null,
});
session.steps.provider_selection.status = "complete";
const { deps, calls } = createDeps({
isInferenceRouteReady: vi.fn(() => true),
repairLocalInferenceSystemdOverrideOrExit: vi.fn(() => {
throw new Error("repair failed");
}),
});

await expect(
handleProviderInferenceState({
...baseOptions(deps, session),
resume: true,
sandboxName: "my-assistant",
}),
).rejects.toThrow("repair failed");

expect(calls.repairEvent).toHaveBeenCalledWith("state.repair.started", {
state: "provider_selection",
metadata: { repair: "ollama-systemd-loopback" },
});
expect(calls.repairEvent).toHaveBeenCalledWith("state.repair.failed", {
state: "provider_selection",
error: "repair failed",
metadata: { repair: "ollama-systemd-loopback" },
});
expect(calls.repairEvent).not.toHaveBeenCalledWith("state.repair.completed", expect.anything());
});

it("reruns inference setup when resumed provider recovery forces recreation", async () => {
const session = createSession({
provider: "compatible-endpoint",
Expand Down
Loading