diff --git a/src/lib/onboard/machine/core-flow-phases.ts b/src/lib/onboard/machine/core-flow-phases.ts index bd4cdc541d9..d318f263238 100644 --- a/src/lib/onboard/machine/core-flow-phases.ts +++ b/src/lib/onboard/machine/core-flow-phases.ts @@ -147,16 +147,19 @@ export async function runCoreOnboardFlowSlice; }): Promise> { - // Compatibility bridge for live resume repair while legacy step helpers and - // OnboardRuntimeBoundary compatibility replay can leave the durable machine - // snapshot already downstream of this slice. The tolerated downstream family - // includes sandbox branch states and the final slice handoff states: openclaw, - // agent_setup, policies, finalizing, and post_verify. Resume still needs to - // re-run provider and sandbox repair/backstop checks before policy or final - // verification handling observes the session. This PR does not fix the - // broader persistence contract because those repairs are not strict FSM states - // yet. Remove this fallback once resume repairs are strict FSM states, or once - // direct legacy step helpers no longer write session.machine. + // Compatibility bridge for live resume repair when durable machine snapshots + // are already downstream of this slice even though provider/sandbox + // repair/backstop checks must still re-run. Those ahead-state snapshots can + // come from legacy/test step mutation that explicitly opts into + // `updateMachine === true` or from repaired-resume replay of persisted + // sessions. This slice cannot eliminate that source locally because the + // repair/backstop checks are still modeled as imperative resume work rather + // than strict FSM recovery states. The tolerated downstream family includes + // sandbox branch states and the final slice handoff states: openclaw, + // agent_setup, policies, finalizing, and post_verify. Phase tests cover + // ahead-state resume and terminal-state rejection; remove this fallback once + // those checks are strict FSM recovery states and legacy machine step mutation + // is gone. return runLiveOnboardFlowSlice({ context: options.context, runtime: options.runtime, diff --git a/src/lib/onboard/machine/final-flow-phases.ts b/src/lib/onboard/machine/final-flow-phases.ts index 91779489b75..645eace9fa5 100644 --- a/src/lib/onboard/machine/final-flow-phases.ts +++ b/src/lib/onboard/machine/final-flow-phases.ts @@ -159,18 +159,18 @@ export async function runFinalOnboardFlowSlice { - // Keep resume and ahead-state sessions on the compatibility path for now. - // The persisted invalid states for this slice are "policies", "finalizing", - // and "post_verify": a previous run may have advanced `session.machine` - // there via legacy step helpers, but resume still needs to re-run branch - // setup/readiness, policy reconciliation, and final verification. Those - // legacy helpers remain a second machine snapshot writer in - // OnboardRuntimeBoundary/recordStateResultWithStepCompatibility, so this - // slice cannot make those persisted states impossible at the source without - // changing the broader step persistence contract. Remove this fallback once - // final-phase repair checks are first-class resumable FSM states, or once - // legacy step helpers no longer advance `session.machine` and handler FSM - // results are the sole transition source. + // Compatibility bridge for live resume repair when durable machine snapshots + // are already downstream of this slice even though branch setup/readiness, + // policy reconciliation, and final verification must still re-run. Those + // ahead-state snapshots can come from legacy/test step mutation that + // explicitly opts into `updateMachine === true` or from repaired-resume replay + // of persisted sessions. This slice cannot eliminate that source locally + // because final-phase repair checks are still modeled as imperative resume + // work rather than strict FSM recovery states. The tolerated downstream states + // are "policies", "finalizing", and "post_verify". Phase tests cover + // ahead-state resume and terminal-state rejection; remove this fallback once + // final-phase repair checks are first-class FSM recovery states and legacy + // machine step mutation is gone. await runLiveOnboardFlowSlice({ context: options.context, runtime: withAfterPoliciesResultApplied(options.runtime, options.afterPoliciesResultApplied), diff --git a/src/lib/onboard/machine/initial-flow-phases.ts b/src/lib/onboard/machine/initial-flow-phases.ts index 145175d8baa..b71e00c90ec 100644 --- a/src/lib/onboard/machine/initial-flow-phases.ts +++ b/src/lib/onboard/machine/initial-flow-phases.ts @@ -190,17 +190,19 @@ export async function runInitialOnboardFlowSlice; }): Promise> { - // Compatibility bridge for live resume repair while legacy step helpers and - // OnboardRuntimeBoundary compatibility replay can leave the durable machine - // snapshot already downstream of this slice. The tolerated downstream family - // is every nonterminal state after the initial slice: inference, sandbox, - // openclaw/agent_setup, policies, finalizing, and post_verify. Resume still - // needs to re-run preflight/gateway host backstops before later provider, - // sandbox, policy, or verification handling observes the session. This PR - // does not fix the broader persistence contract because strict FSM repair - // states must preserve those safety checks first. Remove this fallback once - // resume repairs are strict FSM states, or once direct legacy step helpers no - // longer write session.machine. + // Compatibility bridge for live resume repair when durable machine snapshots + // are already downstream of this slice even though preflight/gateway host + // backstops must still re-run. Those ahead-state snapshots can come from + // legacy/test step mutation that explicitly opts into `updateMachine === true` + // or from repaired-resume replay of persisted sessions. This slice cannot + // eliminate that source locally because the host backstop checks are still + // modeled as imperative resume work rather than strict FSM recovery states. + // The tolerated downstream family is every nonterminal state after the initial + // slice: inference, sandbox, openclaw/agent_setup, policies, finalizing, and + // post_verify. Phase tests cover ahead-state resume and terminal-state + // rejection; remove this fallback once those repair/backstop checks are + // modeled as strict FSM recovery states and legacy machine step mutation is + // gone. return runLiveOnboardFlowSlice({ context: options.context, runtime: options.runtime, diff --git a/src/lib/onboard/runtime-boundary-record-only.test.ts b/src/lib/onboard/runtime-boundary-record-only.test.ts index c4496fdc0af..340ca7e1341 100644 --- a/src/lib/onboard/runtime-boundary-record-only.test.ts +++ b/src/lib/onboard/runtime-boundary-record-only.test.ts @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -import { describe, expect, it } from "vitest"; +import { describe, expect, it, vi } from "vitest"; import { createSession, @@ -115,6 +115,38 @@ describe("OnboardRuntimeBoundary record-only step/result pairing", () => { ]); }); + it("applies validated step completion results directly without compatibility diagnostics", async () => { + const { boundary, events } = createRuntimeHarness(); + const compatibilitySpy = vi + .spyOn(boundary, "recordStateResultWithStepCompatibility") + .mockRejectedValue(new Error("compatibility bridge should not be used")); + + try { + await boundary.recordStateResult(advanceTo("preflight")); + const completed = await boundary.recordStepCompleteWithStateResult( + "preflight", + { sandboxName: "strict-path-sb" }, + advanceTo("gateway", { metadata: { state: "preflight" } }), + ); + + expect(completed).toMatchObject({ + sandboxName: "strict-path-sb", + machine: { state: "gateway", revision: 2 }, + steps: { preflight: { status: "complete" } }, + }); + expect(compatibilitySpy).not.toHaveBeenCalled(); + expect(events.map((event) => event.type)).toEqual([ + "state.exited", + "state.entered", + "state.exited", + "state.entered", + ]); + expect(events.some((event) => event.type === "state.result.skipped")).toBe(false); + } finally { + compatibilitySpy.mockRestore(); + } + }); + it("pairs record-only step failure with an explicit failure result", async () => { const { boundary, events } = createRuntimeHarness(); diff --git a/src/lib/onboard/runtime-boundary.ts b/src/lib/onboard/runtime-boundary.ts index dc9c75ba84d..88e006fc7cd 100644 --- a/src/lib/onboard/runtime-boundary.ts +++ b/src/lib/onboard/runtime-boundary.ts @@ -175,7 +175,7 @@ export class OnboardRuntimeBoundary { ): Promise { await this.assertStateResultWillApply(result); await this.getRuntime().markStepCompleteRecordOnly(stepName, updates); - return this.recordStateResultWithStepCompatibility(result); + return this.recordStateResult(result); } async recordStepFailedWithStateResult( @@ -190,15 +190,14 @@ export class OnboardRuntimeBoundary { /** * Compatibility bridge for two named stale-result sources: - * 1. legacy/test boundaries configured with `updateMachine === true`, where - * step helpers still advance `session.machine` before handler results; + * 1. legacy/test boundaries explicitly configured with `updateMachine === true`; * 2. live flow-slice replay for repaired resume or persisted ahead-state * sessions, where safety checks must re-run even when the durable machine * snapshot is already downstream. * - * Default production record-only paths reject stale transition results before - * applying them. Skipped compatible results must stay metadata-only so stale - * results cannot become a context source. Remove this bridge once resume + * Default record-only paths reject stale transition results before applying + * them. Skipped compatible results must stay metadata-only so stale results + * cannot become a context source. Remove this bridge once resume * repair/ahead-state replay is represented by first-class FSM recovery states * and no boundary opts into legacy machine step mutation. */