diff --git a/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Enrollment/Completion/CompletionStateMachine.cs b/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Enrollment/Completion/CompletionStateMachine.cs index 65fc5003..74664015 100644 --- a/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Enrollment/Completion/CompletionStateMachine.cs +++ b/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Enrollment/Completion/CompletionStateMachine.cs @@ -33,6 +33,7 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Enrollment.Completion public bool IsHelloCompleted { get; set; } public bool IsHelloPolicyConfigured { get; set; } public bool HasUnresolvedEspCategories { get; set; } + public bool HasAccountSetupActivity { get; set; } // WhiteGlove detection signals public bool WhiteGloveStartDetected { get; set; } @@ -287,9 +288,11 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Enrollment.Completion var prev = _currentState; var result = new TransitionResult { PreviousState = prev, SignalsToRecord = new List() }; - // Path A: AccountSetup completed or desktop already arrived + // Path A: AccountSetup completed, desktop already arrived, or provisioning registry confirms AccountSetup + bool accountSetupFromProvisioning = ctx.LastEspPhase == null && ctx.HasAccountSetupActivity; if (string.Equals(ctx.LastEspPhase, "AccountSetup", StringComparison.OrdinalIgnoreCase) - || _desktopArrived) + || _desktopArrived + || accountSetupFromProvisioning) { _espFinalExitSeen = true; _currentState = EnrollmentCompletionState.EspExitedAwaitingCompletion; diff --git a/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Enrollment/EnrollmentTracker.CompletionLogic.cs b/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Enrollment/EnrollmentTracker.CompletionLogic.cs index af3d154e..e7f7a45c 100644 --- a/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Enrollment/EnrollmentTracker.CompletionLogic.cs +++ b/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Enrollment/EnrollmentTracker.CompletionLogic.cs @@ -602,12 +602,22 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Enrollment skipUser = _skipUserStatusPage; } - if (string.Equals(lastPhase, "AccountSetup", StringComparison.OrdinalIgnoreCase) || desktopArr) + // Defense-in-depth: if lastEspPhase is null (state lost after reboot) but the + // ESP provisioning registry shows AccountSetup activity, treat as AccountSetup. + // This catches the edge case where neither persistence nor backfill recovered the phase. + bool accountSetupFromProvisioning = lastPhase == null + && (_espAndHelloTracker?.HasAccountSetupActivity == true); + + if (string.Equals(lastPhase, "AccountSetup", StringComparison.OrdinalIgnoreCase) + || desktopArr + || accountSetupFromProvisioning) { - // Final ESP exit: either AccountSetup phase detected OR desktop arrived (backup) + // Final ESP exit: AccountSetup phase detected, desktop arrived (backup), or provisioning registry confirms AccountSetup var phaseInfo = string.Equals(lastPhase, "AccountSetup", StringComparison.OrdinalIgnoreCase) ? "AccountSetup" - : $"{lastPhase ?? "unknown"} (desktop arrival backup)"; + : accountSetupFromProvisioning + ? "AccountSetup (provisioning registry fallback)" + : $"{lastPhase ?? "unknown"} (desktop arrival backup)"; _logger.Info($"EnrollmentTracker: ESP final exit from {phaseInfo} — marking _espFinalExitSeen, starting Hello wait timer"); lock (_stateLock) diff --git a/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Enrollment/EnrollmentTracker.EventHandlers.cs b/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Enrollment/EnrollmentTracker.EventHandlers.cs index 062fef59..a2f796cc 100644 --- a/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Enrollment/EnrollmentTracker.EventHandlers.cs +++ b/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Enrollment/EnrollmentTracker.EventHandlers.cs @@ -135,6 +135,7 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Enrollment if (_stateData.EspFirstSeenUtc == null) _stateData.EspFirstSeenUtc = DateTime.UtcNow; } + _statePersistence.Save(_stateData); // Immediate persist — ESP phase + espEverSeen must survive reboot RecordSignal($"esp_phase_{phase}"); // ESP phase change means ESP is progressing — cancel any pending failure grace period diff --git a/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Enrollment/EnrollmentTracker.cs b/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Enrollment/EnrollmentTracker.cs index e7935021..da02e7b9 100644 --- a/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Enrollment/EnrollmentTracker.cs +++ b/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Enrollment/EnrollmentTracker.cs @@ -219,6 +219,7 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Enrollment IsHelloCompleted = _espAndHelloTracker?.IsHelloCompleted ?? false, IsHelloPolicyConfigured = _espAndHelloTracker?.IsPolicyConfigured ?? false, HasUnresolvedEspCategories = false, // Will be set by caller when available + HasAccountSetupActivity = _espAndHelloTracker?.HasAccountSetupActivity ?? false, WhiteGloveStartDetected = _espAndHelloTracker?.IsWhiteGloveStartDetected ?? false, HasSaveWhiteGloveSuccessResult = _espAndHelloTracker?.HasSaveWhiteGloveSuccessResult ?? false, AgentStartTimeUtc = _agentStartTimeUtc, @@ -418,6 +419,11 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Enrollment _espAndHelloTracker.WhiteGloveCompleted += OnWhiteGloveCompleted; _espAndHelloTracker.EspFailureDetected += OnEspFailureDetected; _espAndHelloTracker.DeviceSetupProvisioningComplete += OnDeviceSetupProvisioningComplete; + + // Backfill recent ESP exit events that may have fired before we subscribed. + // The EspAndHelloTracker is started by CollectorCoordinator before the EnrollmentTracker + // is created, so esp_exiting events detected during that window are lost. Backfill recovers them. + _espAndHelloTracker.BackfillRecentEspExitEvents(); } } diff --git a/src/Agent/AutopilotMonitor.Agent/buildcounter.txt b/src/Agent/AutopilotMonitor.Agent/buildcounter.txt index 9540e56f..617ba1c1 100644 --- a/src/Agent/AutopilotMonitor.Agent/buildcounter.txt +++ b/src/Agent/AutopilotMonitor.Agent/buildcounter.txt @@ -1 +1 @@ -1006 +1008