mirror of
https://github.com/okieselbach/AutopilotMonitor.git
synced 2026-10-24 04:32:32 +00:00
prevent doubel completion_decision_process events, fix phase timeline
This commit is contained in:
+42
-16
@@ -205,13 +205,31 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Enrollment
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when WhiteGlove (Pre-Provisioning) completes successfully.
|
||||
/// Emits the whiteglove_complete event. The MonitoringService handles
|
||||
/// the actual agent shutdown upon seeing this event type.
|
||||
/// Called when WhiteGlove (Pre-Provisioning) completes successfully (event-handler signature).
|
||||
/// Subscribed to EspAndHelloTracker.WhiteGloveCompleted. Delegates to CompleteWhiteGlove
|
||||
/// which contains the actual emission pipeline.
|
||||
/// </summary>
|
||||
private void OnWhiteGloveCompleted(object sender, EventArgs e)
|
||||
{
|
||||
var wgSource = sender?.GetType().Name ?? "unknown";
|
||||
CompleteWhiteGlove(wgSource, decisionAlreadyEmitted: false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Core WhiteGlove completion pipeline: emits the whiteglove_complete terminal event,
|
||||
/// the decision_process_completion audit record (unless the caller already emitted one),
|
||||
/// stops the summary timer, and updates the shadow state machine. MonitoringService handles
|
||||
/// the actual agent shutdown upon seeing whiteglove_complete.
|
||||
/// Idempotent: fire-once per session instance. A second invocation (e.g. Shell-Core
|
||||
/// watcher fires after the signal-correlated detector already matched) is a no-op.
|
||||
/// </summary>
|
||||
private void OnWhiteGloveCompleted(object sender, EventArgs e)
|
||||
/// <param name="source">Decision source identifier (caller type or descriptor).</param>
|
||||
/// <param name="decisionAlreadyEmitted">
|
||||
/// Set to true when the caller already emitted a decision_process_completion for this
|
||||
/// terminal (e.g. the WG-redirect guard inside TryEmitEnrollmentComplete). Prevents the
|
||||
/// double-decision pattern that loses the upstream decisionChain under a second record.
|
||||
/// </param>
|
||||
private void CompleteWhiteGlove(string source, bool decisionAlreadyEmitted)
|
||||
{
|
||||
lock (_stateLock)
|
||||
{
|
||||
@@ -223,18 +241,21 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Enrollment
|
||||
_signalCorrelatedWhiteGloveTriggered = true;
|
||||
}
|
||||
|
||||
var wgSource = sender?.GetType().Name ?? "unknown";
|
||||
_logger.Info($"EnrollmentTracker: WhiteGlove pre-provisioning completed (source: {wgSource})");
|
||||
_logger.Info($"EnrollmentTracker: WhiteGlove pre-provisioning completed (source: {source})");
|
||||
|
||||
// Emit decision process before the terminal event
|
||||
EmitDecisionProcess("whiteglove_complete", wgSource, "pending_part1", new List<string>
|
||||
// Decision audit: only emit if the caller hasn't already recorded the decision that led here.
|
||||
// Keeps the invariant "one terminal event = one decision_process_completion".
|
||||
if (!decisionAlreadyEmitted)
|
||||
{
|
||||
$"trigger={wgSource}",
|
||||
(_espAndHelloTracker?.HasSaveWhiteGloveSuccessResult ?? false) ? "registry_confirmed" : "registry_not_confirmed",
|
||||
(_espAndHelloTracker?.IsWhiteGloveStartDetected ?? false) ? "event509_seen" : "event509_not_seen",
|
||||
_aadJoinedWithUser ? "user_joined" : "no_user_joined",
|
||||
_fooUserDetected ? "foouser_detected" : "no_foouser"
|
||||
});
|
||||
EmitDecisionProcess("whiteglove_complete", source, "pending_part1", new List<string>
|
||||
{
|
||||
$"trigger={source}",
|
||||
(_espAndHelloTracker?.HasSaveWhiteGloveSuccessResult ?? false) ? "registry_confirmed" : "registry_not_confirmed",
|
||||
(_espAndHelloTracker?.IsWhiteGloveStartDetected ?? false) ? "event509_seen" : "event509_not_seen",
|
||||
_aadJoinedWithUser ? "user_joined" : "no_user_joined",
|
||||
_fooUserDetected ? "foouser_detected" : "no_foouser"
|
||||
});
|
||||
}
|
||||
|
||||
// Stop summary timer — no more app tracking needed
|
||||
_summaryTimer?.Change(System.Threading.Timeout.Infinite, System.Threading.Timeout.Infinite);
|
||||
@@ -1128,17 +1149,22 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Enrollment
|
||||
$"with no user joined and no desktop arrived — redirecting to WhiteGlove Part 1 " +
|
||||
$"(source={source}, aadJoinedWithUser={_aadJoinedWithUser}, desktopArrived={desktopArrived})");
|
||||
|
||||
// Consolidated decision record: includes the upstream source AND the WG-context
|
||||
// signals that used to live in a second (now-suppressed) decision record.
|
||||
EmitDecisionProcess("whiteglove_complete", source, "redirected_wg_part1", new List<string>
|
||||
{
|
||||
$"original_source={source}",
|
||||
"save_whiteglove_succeeded=true",
|
||||
(_espAndHelloTracker?.IsWhiteGloveStartDetected ?? false) ? "event509_seen" : "event509_not_seen",
|
||||
"aad_joined_with_user=false",
|
||||
"desktop_arrived=false",
|
||||
_fooUserDetected ? "foouser_detected" : "no_foouser",
|
||||
"redirect: enrollment_complete -> whiteglove_complete"
|
||||
});
|
||||
|
||||
OnWhiteGloveCompleted(this, EventArgs.Empty);
|
||||
ShadowProcessTrigger("whiteglove_complete");
|
||||
// Decision is already recorded above — CompleteWhiteGlove must not emit a second one.
|
||||
// ShadowProcessTrigger("whiteglove_complete") is called inside CompleteWhiteGlove, so no extra call here.
|
||||
CompleteWhiteGlove(source, decisionAlreadyEmitted: true);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -249,10 +249,13 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Enrollment
|
||||
_stateData.CompletionState = _completionSm.CurrentState.ToString();
|
||||
}
|
||||
|
||||
// Compare terminal state: shadow SM reached terminal vs actual _enrollmentCompleteEmitted
|
||||
// Compare terminal state: shadow SM reached terminal vs actual _enrollmentCompleteEmitted.
|
||||
// Both sides must use the same definition of "terminal": the actual flag is set for
|
||||
// enrollment_complete, enrollment_failed AND the WG-redirect path — so shadow must match
|
||||
// that by accepting Completed, Failed and WhiteGloveCompleted as terminal states.
|
||||
bool actualTerminal;
|
||||
lock (_stateLock) { actualTerminal = _enrollmentCompleteEmitted; }
|
||||
bool shadowTerminal = _completionSm.CurrentState == EnrollmentCompletionState.Completed;
|
||||
bool shadowTerminal = _completionSm.CurrentState.IsTerminal();
|
||||
|
||||
if (actualTerminal != shadowTerminal)
|
||||
{
|
||||
|
||||
@@ -1 +1 @@
|
||||
1023
|
||||
1025
|
||||
|
||||
+5
-1
@@ -709,8 +709,12 @@ namespace AutopilotMonitor.Functions.Functions.Ingest
|
||||
}
|
||||
else if (c.WhiteGloveEvent != null)
|
||||
{
|
||||
// The whiteglove_complete event carries Phase=Complete as a convention
|
||||
// (agent signalling "terminal reached"). Persisting that literally as Session.CurrentPhase
|
||||
// contradicts Status=Pending and tricks the UI timeline into rendering user-enrollment
|
||||
// phases as completed. Cap to AppsDevice (last pre-provisioning phase actually reached).
|
||||
whiteGloveStatusTransitioned = await _sessionRepo.UpdateSessionStatusAsync(
|
||||
request.TenantId, request.SessionId, SessionStatus.Pending, c.WhiteGloveEvent.Phase,
|
||||
request.TenantId, request.SessionId, SessionStatus.Pending, EnrollmentPhase.AppsDevice,
|
||||
earliestEventTimestamp: c.EarliestEventTimestamp, latestEventTimestamp: c.LatestEventTimestamp,
|
||||
isPreProvisioned: true, isUserDriven: false);
|
||||
|
||||
|
||||
@@ -265,6 +265,12 @@ export default function PhaseTimeline({ currentPhase, completedPhases, events =
|
||||
const effectiveCurrentPhase = (() => {
|
||||
if (sessionStatus === 'Succeeded') return currentPhase;
|
||||
if (sessionStatus === 'Failed') return failurePhase !== null ? failurePhase : currentPhase;
|
||||
// WG Part 1 (sealed, awaiting user): trust the already-capped maxEventPhase.
|
||||
// Older sessions may still carry currentPhase=7 (Complete) from before the backend cap;
|
||||
// without this guard the `>=` comparison below would ignore the cap and mark all user phases as completed.
|
||||
if (sessionStatus === 'Pending' && hasWhiteGloveComplete && !hasWhiteGloveResumed) {
|
||||
return maxEventPhase >= 0 ? maxEventPhase : currentPhase;
|
||||
}
|
||||
if (maxEventPhase < 0) return currentPhase;
|
||||
return phaseIndex(maxEventPhase) >= phaseIndex(currentPhase) ? maxEventPhase : currentPhase;
|
||||
})();
|
||||
|
||||
Reference in New Issue
Block a user