Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
7c809c5
refactor(onboard): make recovery and sandbox entry strict
cv Jul 28, 2026
6d2a218
refactor(onboard): make runtime the machine authority
cv Jul 28, 2026
f58ca88
test(onboard): cover exact sandbox phase entry
cv Jul 28, 2026
a7266db
merge(onboard): refresh FSM stack base
cv Jul 28, 2026
4e91c88
merge(onboard): refresh FSM stack from main
cv Jul 28, 2026
9b8ee24
merge(onboard): refresh FSM stack base
cv Jul 28, 2026
03a4341
chore(architecture): ratchet state root files
cv Jul 28, 2026
13ba3df
docs(onboard): state runtime ownership contract
cv Jul 28, 2026
4788d97
merge(onboard): refresh FSM stack from main
cv Jul 28, 2026
90dd9cd
merge(onboard): refresh FSM stack base
cv Jul 28, 2026
83d454f
merge: resolve conflicts with main
github-actions[bot] Aug 3, 2026
6650e85
fix(onboard): resolve runtime authority stack refresh
cv Aug 3, 2026
dc1ccb3
test(onboard): allow gateway reconcile coverage slack
cv Aug 3, 2026
ca084ef
refactor(onboard): remove legacy step mutation API
cv Aug 3, 2026
8fed8a7
refactor(onboard): remove duplicate step mutation API
cv Aug 3, 2026
5ea6980
test(package): allow policy boundary package slack
cv Aug 3, 2026
c85a048
fix(onboard): complete step mutation cleanup
cv Aug 3, 2026
46f8196
Merge remote-tracking branch 'origin/codex/onboard-fsm-runtime-author…
cv Aug 3, 2026
b1263d8
test(architecture): keep mutation absence guard linear
cv Aug 3, 2026
e78098d
Merge remote-tracking branch 'origin/codex/onboard-fsm-runtime-author…
cv Aug 3, 2026
a7c3fb9
fix(onboard): preserve interrupted session guidance
cv Aug 3, 2026
6a7a061
Merge remote-tracking branch 'origin/main' into codex/merge-train-7716
cv Aug 3, 2026
b1f9f0c
fix(onboard): preserve terminal transition errors
cv Aug 3, 2026
bcd3c03
merge: resolve conflicts with main
github-actions[bot] Aug 3, 2026
8381e19
merge(main): refresh #7716 with current main
cv Aug 3, 2026
48a2766
docs(onboard): clarify transition bookkeeping
cv Aug 3, 2026
dfd7278
docs(onboard): align step bookkeeping terms
cv Aug 3, 2026
73648b4
merge(onboard): refresh #7716 on current main
cv Aug 3, 2026
6f9db55
merge: refresh PR #7716 from main
cv Aug 3, 2026
e2dcffb
test(onboard): correct runtime boundary descriptions
cv Aug 3, 2026
288ebe2
merge: refresh #7716 onto main
cv Aug 3, 2026
26d4549
fix(onboard): restore skipped-step events
cv Aug 3, 2026
7c83651
merge: refresh #7716 onto main
cv Aug 3, 2026
7dfbb44
merge: resolve conflicts with main
github-actions[bot] Aug 3, 2026
c1552c6
refactor(onboard): extract flow handoff context (#7717)
cv Aug 3, 2026
49f779c
Merge branch 'main' into codex/onboard-fsm-runtime-authority
cv Aug 4, 2026
8da6eb3
merge(main): refresh #7716 after MCP runtime remediation
cv Aug 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
19 changes: 8 additions & 11 deletions src/lib/onboard/machine/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,15 +41,12 @@ A state handler may perform many smaller operations, but it should expose only s

## Session steps versus machine state

The persisted onboarding session still tracks step-level progress for resumability. Step recording is older than the FSM and is currently used as a compatibility bridge.
The persisted onboarding session tracks step-level progress for resumability.

Long term:

- `OnboardRuntime` should own machine transitions and machine revision increments.
- Session step helpers should record only step status (`pending`, `in_progress`, `complete`, `failed`, `skipped`).
- State handlers should return explicit results instead of implicitly moving the machine by calling step helpers.

Until that migration completes, step helpers may still infer machine snapshots for compatibility with older sessions and tests.
- `OnboardRuntime` owns normal machine transitions, revision increments, terminal state, and machine events.
- Session step helpers record only step status (`pending`, `in_progress`, `complete`, `failed`, `skipped`) and safe step updates. They cannot change the machine snapshot.
- State handlers return explicit results. They do not move the machine through step helpers.
- Explicit session recovery and the process-exit failure backstop are narrow exceptions.

## Handler contract

Expand Down Expand Up @@ -87,15 +84,15 @@ without a state transition so a later process can resume the same non-terminal s

## Runtime responsibilities

`OnboardRuntime` is the intended authority for:
`OnboardRuntime` is the authority for:

- validating transitions against `transitions.ts`;
- validating result source, target, kind, and graph transitions;
- applying safe session context updates;
- marking terminal states;
- emitting redacted lifecycle, state, repair, resume-conflict, and hook events;
- normalizing older sessions before strict execution.

The runtime should reject invalid transitions before they can be persisted.
Step helpers record step status and safe step updates. They cannot change the machine snapshot or emit machine events. Explicit session recovery and the process-exit failure backstop are separate recovery boundaries. They validate their snapshot changes and run before or outside handler execution.

## Event semantics

Expand Down
14 changes: 8 additions & 6 deletions src/lib/onboard/machine/core-flow-phases.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ function context(
hermesToolGateways: [],
preferredInferenceApi: null,
compatibleEndpointReasoning: null,

compatibleEndpointReasoningEffort: null,
nimContainer: null,
webSearchConfig: null,
Expand Down Expand Up @@ -136,6 +137,7 @@ function createPhases(
hermesToolGateways: ["local"],
preferredInferenceApi: "chat",
compatibleEndpointReasoning: null,

compatibleEndpointReasoningEffort: null,
nimContainer: "nim-test",
})),
Expand All @@ -161,8 +163,10 @@ function createPhases(
recordRepairEvent: vi.fn(async () => createSession()),
hydrateCredentialEnv: vi.fn(),
configureCompatibleEndpointReasoning: vi.fn(async () => "false" as const),
clearCompatibleEndpointReasoning: vi.fn(() => null),

configureCompatibleEndpointReasoningEffort: vi.fn(async () => null),
clearCompatibleEndpointReasoning: vi.fn(() => null),

clearCompatibleEndpointReasoningEffort: vi.fn(() => null),
repairLocalInferenceSystemdOverrideOrExit: vi.fn(),
isNonInteractive: () => true,
Expand Down Expand Up @@ -204,11 +208,8 @@ function createPhases(
resolvePath: (value) => value,
agentSupportsWebSearch: () => true,
note: vi.fn(),

cliName: () => "nemoclaw",
retireReplacedSandboxWorkload: vi.fn(() => ({
status: "skipped" as const,
reason: "replacement-unproven" as const,
})),
updateSession: vi.fn((mutator) => mutator(createSession()) ?? createSession()),
getStoredMessagingChannelConfig: () => null,
hydrateMessagingChannelConfig: (config) => config,
Expand Down Expand Up @@ -371,6 +372,7 @@ describe("core onboard flow phases", () => {
hermesToolGateways: [],
preferredInferenceApi: "chat",
compatibleEndpointReasoning: null,

compatibleEndpointReasoningEffort: null,
nimContainer: null,
}));
Expand Down Expand Up @@ -860,7 +862,7 @@ describe("core onboard flow phases", () => {
resume: true,
recordRepairEvent: repairRecorder(),
}),
).rejects.toThrow("Unexpected onboarding live flow state before slice entry");
).rejects.toThrow("Unexpected onboarding flow state before slice entry");
expect(providerInference.run).not.toHaveBeenCalled();
expect(sandbox.run).not.toHaveBeenCalled();
});
Expand Down
8 changes: 6 additions & 2 deletions src/lib/onboard/machine/core-flow-phases.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import {
type ProviderInferenceStateOptions,
} from "./handlers/provider-inference";
import { handleSandboxState, type SandboxStateOptions } from "./handlers/sandbox";
import { UnexpectedLiveOnboardFlowSliceStateError } from "./live-flow-slice";
import { UnexpectedOnboardFlowSliceStateError } from "./flow-slice-error";
import {
type OnboardPrerequisiteRepairEventRecorder,
runOnboardPrerequisiteRepair,
Expand Down Expand Up @@ -67,6 +67,7 @@ export interface SandboxOnboardFlowPhaseOptions<
gatewayName: string;
authoritativeResumeConfig?: boolean;
authoritativePolicyTier?: string | null;

recreateJournalTargetIntentFingerprint?: string | null;
resumeAgentChanged: boolean;
requestedObservabilityEnabled?: boolean | null;
Expand Down Expand Up @@ -159,6 +160,7 @@ export function createProviderInferenceOnboardFlowPhase<
hermesToolGateways: context.hermesToolGateways,
preferredInferenceApi: context.preferredInferenceApi,
compatibleEndpointReasoning: context.compatibleEndpointReasoning,

compatibleEndpointReasoningEffort: context.compatibleEndpointReasoningEffort,
nimContainer: context.nimContainer,
webSearchConfig: context.webSearchConfig,
Expand All @@ -183,6 +185,7 @@ export function createProviderInferenceOnboardFlowPhase<
hermesToolGateways: providerInferenceResult.hermesToolGateways,
preferredInferenceApi: providerInferenceResult.preferredInferenceApi,
compatibleEndpointReasoning: providerInferenceResult.compatibleEndpointReasoning,

compatibleEndpointReasoningEffort:
providerInferenceResult.compatibleEndpointReasoningEffort,
nimContainer: providerInferenceResult.nimContainer,
Expand Down Expand Up @@ -214,6 +217,7 @@ export function createSandboxOnboardFlowPhase<
gatewayName: options.gatewayName,
authoritativeResumeConfig: options.authoritativeResumeConfig,
authoritativePolicyTier: options.authoritativePolicyTier,

recreateJournalTargetIntentFingerprint: options.recreateJournalTargetIntentFingerprint,
endpointSource: endpointProvenance.endpointSource,
resumeAgentChanged: options.resumeAgentChanged,
Expand Down Expand Up @@ -279,7 +283,7 @@ export async function runCoreOnboardFlowSlice<Context extends OnboardFlowContext
]
: ["provider_selection", "inference", "sandbox", "openclaw", "agent_setup"];
if (!allowedStates.includes(state)) {
throw new UnexpectedLiveOnboardFlowSliceStateError(
throw new UnexpectedOnboardFlowSliceStateError(
state,
["provider_selection", "inference", "sandbox"],
allowedStates.filter(
Expand Down
4 changes: 2 additions & 2 deletions src/lib/onboard/machine/final-flow-phases.runtime.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
import { createSession } from "../../state/onboard-session";
import type { VerifyDeploymentResult } from "../../verify-deployment";
import { runFinalOnboardFlowSlice } from "./final-flow-phases";
import { UnexpectedLiveOnboardFlowSliceStateError } from "./live-flow-slice";
import { UnexpectedOnboardFlowSliceStateError } from "./flow-slice-error";

function deploymentResult(healthy: boolean): VerifyDeploymentResult {
return {
Expand Down Expand Up @@ -186,7 +186,7 @@ describe("final onboard flow runtime boundary", () => {
phases: createPhases(branchState, order),
recordRepairEvent,
}),
).rejects.toBeInstanceOf(UnexpectedLiveOnboardFlowSliceStateError);
).rejects.toBeInstanceOf(UnexpectedOnboardFlowSliceStateError);

expect(order).toEqual([]);
expect(recordRepairEvent).not.toHaveBeenCalled();
Expand Down
4 changes: 2 additions & 2 deletions src/lib/onboard/machine/final-flow-phases.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import {
handlePostVerifyState,
} from "./handlers/finalization";
import { handlePoliciesState, type PoliciesStateOptions } from "./handlers/policies";
import { UnexpectedLiveOnboardFlowSliceStateError } from "./live-flow-slice";
import { UnexpectedOnboardFlowSliceStateError } from "./flow-slice-error";
import { createPhaseProgressReporter } from "./phase-progress";
import type { OnboardStateResult } from "./result";
import type { OnboardMachineRunnerRuntime, OnboardStateHandlerResult } from "./runner";
Expand Down Expand Up @@ -336,7 +336,7 @@ export async function runFinalOnboardFlowSlice<Context extends OnboardFlowContex
const durableEntry = await options.runtime.session();
const allowedStates = [branchState, ...FINAL_FLOW_DOWNSTREAM_STATES] as const;
if (!allowedStates.includes(durableEntry.machine.state as (typeof allowedStates)[number])) {
throw new UnexpectedLiveOnboardFlowSliceStateError(
throw new UnexpectedOnboardFlowSliceStateError(
durableEntry.machine.state,
[branchState],
FINAL_FLOW_DOWNSTREAM_STATES,
Expand Down
15 changes: 15 additions & 0 deletions src/lib/onboard/machine/flow-slice-error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import type { OnboardMachineState } from "./types";

export class UnexpectedOnboardFlowSliceStateError extends Error {
constructor(
readonly state: OnboardMachineState,
readonly runStates: readonly OnboardMachineState[],
readonly repairStates: readonly OnboardMachineState[],
) {
super(`Unexpected onboarding flow state before slice entry: ${state}`);
this.name = "UnexpectedOnboardFlowSliceStateError";
}
}
3 changes: 2 additions & 1 deletion src/lib/onboard/machine/initial-flow-phases.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ function context(overrides: Partial<Context> = {}): Context {
hermesToolGateways: [],
preferredInferenceApi: null,
compatibleEndpointReasoning: null,

compatibleEndpointReasoningEffort: null,
nimContainer: null,
webSearchConfig: null,
Expand Down Expand Up @@ -593,7 +594,7 @@ describe("initial onboard flow phases", () => {
resume: true,
recordRepairEvent: repairRecorder(),
}),
).rejects.toThrow("Unexpected onboarding live flow state before slice entry");
).rejects.toThrow("Unexpected onboarding flow state before slice entry");
expect(phase.run).not.toHaveBeenCalled();
});

Expand Down
4 changes: 2 additions & 2 deletions src/lib/onboard/machine/initial-flow-phases.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
type PreflightSandboxGpuFlag,
type PreflightStateOptions,
} from "./handlers/preflight";
import { UnexpectedLiveOnboardFlowSliceStateError } from "./live-flow-slice";
import { UnexpectedOnboardFlowSliceStateError } from "./flow-slice-error";
import {
type OnboardPrerequisiteRepairEventRecorder,
runOnboardPrerequisiteRepair,
Expand Down Expand Up @@ -211,7 +211,7 @@ export async function runInitialOnboardFlowSlice<Context extends OnboardFlowCont
? ["init", "preflight", ...resumeAheadStates]
: ["init", "preflight", "gateway", "provider_selection"];
if (!allowedStates.includes(state)) {
throw new UnexpectedLiveOnboardFlowSliceStateError(
throw new UnexpectedOnboardFlowSliceStateError(
state,
["init", "preflight"],
allowedStates.filter((candidate) => candidate !== "init" && candidate !== "preflight"),
Expand Down
Loading
Loading