fix(decision-core): Hello-disabled fast-path in HandleDesktopArrivedV1 (PR2/3)

When HelloPolicyEnabled is explicitly false AND AccountSetup has been reached,
synthesise HelloOutcome="Skipped" on DesktopArrived and route directly through
Finalizing instead of waiting out the full HelloSafety 300s window. This is the
belt-and-suspenders companion to PR1's EspExiting reducer path: whichever fires
first wins, and the dual-path design survives a future regression in either
half (missing EspExiting adapter or missing HelloPolicyDetected adapter).

Both guards are required:
- HelloPolicyEnabled?.Value == false (policy explicitly disabled)
- AccountSetupEnteredUtc != null (mirrors ShouldTransitionToAwaitingHello)

This prevents a premature mid-DeviceSetup completion if HelloPolicyDetected
happens to fire before AccountSetup is observed. Unknown policy
(HelloPolicyEnabled == null) keeps the prior pessimistic behaviour.

The fast-path cancels HelloSafety on its way through so a deadline armed by an
earlier EspExiting cannot fire post-Completion and dead-end in the reducer.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Oliver Kieselbach
2026-05-13 12:50:03 +02:00
parent 879b89d711
commit a32706dee8
@@ -243,6 +243,15 @@ namespace AutopilotMonitor.DecisionCore.Engine
/// Handle <see cref="DecisionSignalKind.DesktopArrived"/>. Mirror of the Hello handler:
/// records <see cref="DecisionState.DesktopArrivedUtc"/>, and if Hello has already
/// resolved the session completes here.
/// <para>
/// Hello-disabled fast-path: when <see cref="DecisionState.HelloPolicyEnabled"/> is
/// explicitly <c>false</c> AND we have reached AccountSetup (so the Hello-skip is the
/// genuine post-ESP state rather than a premature mid-DeviceSetup classification),
/// synthesise <c>HelloOutcome="Skipped"</c> here and route directly through Finalizing.
/// This avoids waiting out the full 5-min HelloSafety window when the policy reader
/// already told us no Hello wizard is expected. Belt-and-suspenders alongside the
/// EspExiting → HelloSafety reducer path: the path that fires first wins.
/// </para>
/// </summary>
private DecisionStep HandleDesktopArrivedV1(DecisionState state, DecisionSignal signal)
{
@@ -254,6 +263,32 @@ namespace AutopilotMonitor.DecisionCore.Engine
var helloAlreadyResolved = state.HelloResolvedUtc != null;
// Hello-disabled fast-path. Both guards required:
// 1. HelloPolicyEnabled?.Value == false — policy reader confirmed no Hello wizard
// 2. AccountSetupEnteredUtc != null — same gate as ShouldTransitionToAwaitingHello,
// prevents a premature pre-AccountSetup completion if HelloPolicyDetected fires
// before AccountSetup is observed.
// Skipping unknown policy (HelloPolicyEnabled == null) preserves the prior pessimistic
// behaviour: keep waiting for Hello / HelloSafety.
if (!helloAlreadyResolved
&& state.HelloPolicyEnabled?.Value == false
&& state.AccountSetupEnteredUtc != null)
{
builder.HelloResolvedUtc = new SignalFact<DateTime>(signal.OccurredAtUtc, signal.SessionSignalOrdinal);
builder.HelloOutcome = new SignalFact<string>("Skipped", signal.SessionSignalOrdinal);
// Cancel HelloSafety if it was armed by an earlier EspExiting — we're completing
// now via the fast-path, the deadline would otherwise fire post-Completion and
// dead-end in the reducer.
builder.CancelDeadline(DeadlineNames.HelloSafety);
return TransitionToFinalizing(
state: state,
signal: signal,
preparedBuilder: builder,
nextStepIndex: nextStep,
trigger: nameof(DecisionSignalKind.DesktopArrived) + ":HelloDisabledFastPath");
}
// Plan §5 Fix 6: mirror of HandleHelloResolvedV1. When both prerequisites are in,
// go through Finalizing (non-terminal) instead of jumping straight to Completed.
if (helloAlreadyResolved)