diff --git a/src/Agent/AutopilotMonitor.Agent.V2.Core.Tests/Monitoring/SystemSignals/EspPolicyProviderStallDetectorTests.cs b/src/Agent/AutopilotMonitor.Agent.V2.Core.Tests/Monitoring/SystemSignals/EspPolicyProviderStallDetectorTests.cs index d6b2ebb8..9eb6a821 100644 --- a/src/Agent/AutopilotMonitor.Agent.V2.Core.Tests/Monitoring/SystemSignals/EspPolicyProviderStallDetectorTests.cs +++ b/src/Agent/AutopilotMonitor.Agent.V2.Core.Tests/Monitoring/SystemSignals/EspPolicyProviderStallDetectorTests.cs @@ -185,6 +185,46 @@ namespace AutopilotMonitor.Agent.V2.Core.Tests.Monitoring.SystemSignals Assert.False(EspPolicyProviderSnapshot.Empty.SidecarRegistered); } + // ------------------------------------------------- phase gate for DevicePreparation providers + + [Theory] + [InlineData("device_preparation", true)] + [InlineData("device_preparation_failed", true)] // a failed device preparation keeps its culprit + [InlineData("none", true)] // unknown phase → keep observing + [InlineData("unavailable", true)] + [InlineData("", true)] + [InlineData("device_preparation_done", false)] + [InlineData("device_setup", false)] + [InlineData("device_setup_failed", false)] + [InlineData("account_setup", false)] + [InlineData("esp_done", false)] + public void Device_preparation_counts_until_it_is_done(string phase, bool expected) + => Assert.Equal(expected, EspPhaseSnapshot.DevicePreparationOpen(phase)); + + [Fact] + public void DevicePreparation_provider_past_its_phase_drops_out_while_setupApps_still_counts() + { + // Field shape (client installed as a Win32 app): the client registers its + // DevicePreparation provider after its own install with InstallationState=1, long + // after device preparation ended. Nothing ever completes it. + var snapshot = Snapshot( + DevicePrep("ConfigMgr", installState: 1), + SetupApps("Contoso", tracking: 0)); + var seen = new Dictionary + { + ["devicePreparation|device|ConfigMgr"] = T0, + ["setupApps|device|Contoso"] = T0, + }; + + var eval = EspPolicyProviderStallDetector.EvaluateStallTransition( + snapshot, seen, new HashSet(), null, false, T0.AddMinutes(30), Dwell, + devicePreparationOpen: false); + + Assert.False(eval.UpdatedFirstSeenUtc.ContainsKey("devicePreparation|device|ConfigMgr")); // reset + var stalled = Assert.Single(eval.NewlyStalled); + Assert.Equal("Contoso", stalled.Provider.Name); + } + // ------------------------------------------------- arm 2: sidecar-missing transition [Fact] @@ -265,17 +305,27 @@ namespace AutopilotMonitor.Agent.V2.Core.Tests.Monitoring.SystemSignals public void Dispose() => _tmp.Dispose(); + // The phase defaults to "none" so the tests never read the test machine's own ESP state. private (EspPolicyProviderStallDetector sut, FakeSignalIngressSink sink, VirtualClock clock) - BuildDetector(StartupEventGate? gate = null) + BuildDetector(StartupEventGate? gate = null, Func? phase = null) { var logger = new AgentLogger(_tmp.Path, AgentLogLevel.Info); var sink = new FakeSignalIngressSink(); var clock = new VirtualClock(At); var post = new InformationalEventPost(sink, clock); - var sut = new EspPolicyProviderStallDetector("S1", "T1", post, logger, clock, gate); + var sut = new EspPolicyProviderStallDetector("S1", "T1", post, logger, clock, gate, + espPhaseReader: phase ?? (() => EspPhaseSnapshot.None)); return (sut, sink, clock); } + private static readonly TimeSpan DwellSpan = TimeSpan.FromMinutes(EspPolicyProviderStallDetector.DwellMinutes); + + private static PolicyProviderState SidecarDone() => + new PolicyProviderState("Sidecar", "device", EspPolicyProviderProbe.KindSetupApps, 1, null); + + private static PolicyProviderState ConfigMgrDevicePrep(int? installationState) => + new PolicyProviderState("ConfigMgr", "device", EspPolicyProviderProbe.KindDevicePreparation, null, installationState); + private static EspPolicyProviderSnapshot ConfigMgrOnly() => new EspPolicyProviderSnapshot(new[] { new PolicyProviderState("ConfigMgr", "device", EspPolicyProviderProbe.KindSetupApps, @@ -435,6 +485,104 @@ namespace AutopilotMonitor.Agent.V2.Core.Tests.Monitoring.SystemSignals } } + [Fact] + public void Provider_registered_after_device_preparation_never_emits() + { + // The ConfigMgr client as a Win32 app: its DevicePreparation provider shows up during + // device setup with InstallationState=1 and stays there. + var snapshot = new EspPolicyProviderSnapshot(new[] { SidecarDone(), ConfigMgrDevicePrep(1) }); + using var _probe = new EspPolicyProviderProbe.ScopedOverride(_ => snapshot); + var (sut, sink, clock) = BuildDetector(phase: () => "device_setup"); + + sut.Tick(); + clock.Advance(TimeSpan.FromHours(2)); + sut.Tick(); + + Assert.Empty(StalledEvents(sink)); + } + + [Fact] + public void A_real_device_preparation_hang_still_emits() + { + // Field shape: only ConfigMgr providers registered, the ESP never left device + // preparation and failed with 0x800705b4. + var snapshot = new EspPolicyProviderSnapshot(new[] + { + new PolicyProviderState("ConfigMgr", "device", EspPolicyProviderProbe.KindSetupApps, null, null), + ConfigMgrDevicePrep(1), + }); + using var _probe = new EspPolicyProviderProbe.ScopedOverride(_ => snapshot); + var (sut, sink, clock) = BuildDetector(phase: () => EspPhaseSnapshot.DevicePreparation); + + sut.Tick(); + clock.Advance(DwellSpan); + sut.Tick(); + + var evt = Assert.Single(StalledEvents(sink)); + var data = Assert.IsAssignableFrom>(evt.TypedPayload); + var stalled = Assert.IsAssignableFrom>>(data["stalledProviders"]); + Assert.Contains(stalled, s => (string)s["kind"] == EspPolicyProviderProbe.KindDevicePreparation); + } + + [Fact] + public void Device_preparation_stall_message_names_the_provider_timeout() + { + var snapshot = new EspPolicyProviderSnapshot(new[] { SidecarDone(), ConfigMgrDevicePrep(1) }); + using var _probe = new EspPolicyProviderProbe.ScopedOverride(_ => snapshot); + var (sut, sink, clock) = BuildDetector(phase: () => EspPhaseSnapshot.DevicePreparation); + + sut.Tick(); + clock.Advance(DwellSpan); + sut.Tick(); + + var message = Assert.Single(StalledEvents(sink)).Payload![SignalPayloadKeys.Message]; + Assert.Contains("until their timeout", message); + Assert.DoesNotContain("without any timeout", message); + } + + [Fact] + public void Fingerprint_skips_device_preparation_providers_past_their_phase() + { + var logger = new AgentLogger(_tmp.Path, AgentLogLevel.Info); + var phase = EspPhaseSnapshot.DevicePreparation; + var devicePrepOnly = new EspPolicyProviderSnapshot(new[] { SidecarDone(), ConfigMgrDevicePrep(1) }); + var withContoso = new EspPolicyProviderSnapshot(new[] + { + SidecarDone(), + ConfigMgrDevicePrep(1), + new PolicyProviderState("Contoso", "device", EspPolicyProviderProbe.KindSetupApps, 0, null), + }); + var current = devicePrepOnly; + + // Run 1: the DevicePreparation provider stalls during device preparation; later, in + // device setup, a Setup/Apps provider stalls as well. + using (var _probe = new EspPolicyProviderProbe.ScopedOverride(_ => current)) + { + var (sut, sink, clock) = BuildDetector(new StartupEventGate(_tmp.Path, logger), () => phase); + sut.Tick(); + clock.Advance(DwellSpan); + sut.Tick(); + + phase = "device_setup"; + current = withContoso; + sut.Tick(); + clock.Advance(DwellSpan); + sut.Tick(); + Assert.Equal(2, StalledEvents(sink).Count); + } + + // Run 2 (restart in device setup): the same Contoso stall lands on the same + // fingerprint only if the stale DevicePreparation provider stayed out of it. + using (var _probe = new EspPolicyProviderProbe.ScopedOverride(_ => withContoso)) + { + var (sut, sink, clock) = BuildDetector(new StartupEventGate(_tmp.Path, logger), () => "device_setup"); + sut.Tick(); + clock.Advance(DwellSpan); + sut.Tick(); + Assert.Empty(StalledEvents(sink)); + } + } + [Fact] public void Healthy_or_absent_tracking_never_emits() { diff --git a/src/Agent/AutopilotMonitor.Agent.V2.Core/Monitoring/Enrollment/SystemSignals/EspPhaseSnapshot.cs b/src/Agent/AutopilotMonitor.Agent.V2.Core/Monitoring/Enrollment/SystemSignals/EspPhaseSnapshot.cs index 29c29de4..66982472 100644 --- a/src/Agent/AutopilotMonitor.Agent.V2.Core/Monitoring/Enrollment/SystemSignals/EspPhaseSnapshot.cs +++ b/src/Agent/AutopilotMonitor.Agent.V2.Core/Monitoring/Enrollment/SystemSignals/EspPhaseSnapshot.cs @@ -20,13 +20,15 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Enrollment.SystemSignals { internal const string None = "none"; internal const string Unavailable = "unavailable"; + internal const string DevicePreparation = "device_preparation"; + private const string FailedSuffix = "_failed"; internal enum CategoryState { Absent, Running, Done, Failed } // ESP order: device preparation, device setup, account setup. private static readonly (string ValueName, string Phase)[] Categories = { - ("DevicePreparationCategory.Status", "device_preparation"), + ("DevicePreparationCategory.Status", DevicePreparation), ("DeviceSetupCategory.Status", "device_setup"), ("AccountSetupCategory.Status", "account_setup"), }; @@ -60,7 +62,7 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Enrollment.SystemSignals var states = Categories.Select((_, i) => Classify(i < statusJson.Count ? statusJson[i] : null)).ToList(); for (var i = 0; i < Categories.Length; i++) - if (states[i] == CategoryState.Failed) return Categories[i].Phase + "_failed"; + if (states[i] == CategoryState.Failed) return Categories[i].Phase + FailedSuffix; for (var i = 0; i < Categories.Length; i++) if (states[i] == CategoryState.Running) return Categories[i].Phase; @@ -69,6 +71,18 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Enrollment.SystemSignals return lastDone == Categories.Length - 1 ? "esp_done" : Categories[lastDone].Phase + "_done"; } + /// + /// Whether device preparation can still hold the ESP in a result: it + /// runs or failed, or the phase is unknown (, ). + /// False once device preparation is done, whichever later phase the ESP is in. + /// + internal static bool DevicePreparationOpen(string phase) => + string.IsNullOrEmpty(phase) + || phase == None + || phase == Unavailable + || phase == DevicePreparation + || phase == DevicePreparation + FailedSuffix; + /// /// One category value: done (categorySucceeded=true, or every subcategory succeeded /// or notRequired), failed (false, or a failed subcategory), running (anything diff --git a/src/Agent/AutopilotMonitor.Agent.V2.Core/Monitoring/Enrollment/SystemSignals/EspPolicyProviderStallDetector.cs b/src/Agent/AutopilotMonitor.Agent.V2.Core/Monitoring/Enrollment/SystemSignals/EspPolicyProviderStallDetector.cs index 5fb9f30d..6da60a70 100644 --- a/src/Agent/AutopilotMonitor.Agent.V2.Core/Monitoring/Enrollment/SystemSignals/EspPolicyProviderStallDetector.cs +++ b/src/Agent/AutopilotMonitor.Agent.V2.Core/Monitoring/Enrollment/SystemSignals/EspPolicyProviderStallDetector.cs @@ -39,6 +39,12 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Enrollment.SystemSignals /// dispatch-guard exempt), never into decision state. /// /// + /// Phase gate: DevicePreparation providers count only while + /// holds. Once device preparation is done + /// their values hold nothing: a ConfigMgr client installed as a Win32 app registers its + /// provider after its own install with InstallationState=1, and nothing completes it. + /// + /// /// Duplicate suppression: in-process one-shot per provider key, plus a /// fingerprint over the sorted set of currently-stalled keys so /// an agent restart into the same stall does not re-report it — while a NEW provider joining @@ -75,6 +81,7 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Enrollment.SystemSignals private readonly IClock _clock; private readonly StartupEventGate? _startupGate; private readonly TimeSpan _dwell; + private readonly Func _espPhaseReader; private readonly Dictionary _firstSeenIncompleteUtc = new Dictionary(StringComparer.OrdinalIgnoreCase); @@ -94,7 +101,8 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Enrollment.SystemSignals AgentLogger logger, IClock clock, StartupEventGate? startupGate = null, - TimeSpan? dwell = null) + TimeSpan? dwell = null, + Func? espPhaseReader = null) { _sessionId = sessionId; _tenantId = tenantId; @@ -103,6 +111,7 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Enrollment.SystemSignals _clock = clock ?? throw new ArgumentNullException(nameof(clock)); _startupGate = startupGate; _dwell = dwell ?? TimeSpan.FromMinutes(DwellMinutes); + _espPhaseReader = espPhaseReader ?? EspPhaseSnapshot.Read; } /// Host timer callback and test seam — one probe + evaluation round. @@ -111,7 +120,8 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Enrollment.SystemSignals var snapshot = EspPolicyProviderProbe.Read(_logger); var eval = EvaluateStallTransition( snapshot, _firstSeenIncompleteUtc, _firedKeys, - _sidecarMissingSinceUtc, _sidecarMissingFired, _clock.UtcNow, _dwell); + _sidecarMissingSinceUtc, _sidecarMissingFired, _clock.UtcNow, _dwell, + EspPhaseSnapshot.DevicePreparationOpen(_espPhaseReader())); _firstSeenIncompleteUtc.Clear(); foreach (var kv in eval.UpdatedFirstSeenUtc) @@ -133,6 +143,7 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Enrollment.SystemSignals /// Arm-2 one-shot latch (never re-report). /// Evaluation timestamp. /// Continuous-condition threshold (shared by both arms). + /// Whether device preparation can still hold the ESP (); false drops DevicePreparation providers from arm 1. internal static StallEvaluation EvaluateStallTransition( EspPolicyProviderSnapshot snapshot, IReadOnlyDictionary firstSeenIncompleteUtc, @@ -140,9 +151,10 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Enrollment.SystemSignals DateTime? sidecarMissingSinceUtc, bool sidecarMissingAlreadyFired, DateTime nowUtc, - TimeSpan dwell) + TimeSpan dwell, + bool devicePreparationOpen = true) { - var result = new StallEvaluation(); + var result = new StallEvaluation { DevicePreparationOpen = devicePreparationOpen }; // Missing root key or no registered providers = normal early enrollment: clear all // dwell state (a condition must be continuously OBSERVED to accrue dwell). @@ -152,8 +164,8 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Enrollment.SystemSignals // Arm 1 — a registered provider continuously incomplete. foreach (var provider in snapshot.Providers) { - if (provider.IsComplete) - continue; // completed → falls out of the dwell map (reset) + if (provider.IsComplete || !result.Counts(provider)) + continue; // completed or past its phase → falls out of the dwell map (reset) var firstSeen = firstSeenIncompleteUtc.TryGetValue(provider.Key, out var seen) ? seen @@ -197,7 +209,7 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Enrollment.SystemSignals // the same stall re-accrues dwell and lands on the same fingerprint → suppressed; a // new condition joining the stall changes it → re-emitted. var currentlyStalledKeys = snapshot.Providers - .Where(p => !p.IsComplete && _firedKeys.Contains(p.Key)) + .Where(p => !p.IsComplete && eval.Counts(p) && _firedKeys.Contains(p.Key)) .Select(p => p.Key) .ToList(); if (_sidecarMissingFired && HasSetupAppsProvider(snapshot) && !snapshot.SidecarRegistered) @@ -259,9 +271,15 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Enrollment.SystemSignals if (eval.SidecarMissingStalledForMinutes != null) data["sidecarMissingForMinutes"] = Math.Round(eval.SidecarMissingStalledForMinutes.Value, 1); + // The Setup/Apps wait has no timeout; device preparation gives up after the provider's + // Timeout node (CSP default 15 min). + var setupAppsWait = eval.SidecarMissingStalledForMinutes != null + || eval.NewlyStalled.Any(s => s.Provider.Kind == EspPolicyProviderProbe.KindSetupApps); var message = - "ESP Setup/Apps tracking stalled: " + string.Join("; ", messageParts) + - " — the ESP waits on its app-tracking providers without any timeout"; + "ESP policy provider stalled: " + string.Join("; ", messageParts) + + (setupAppsWait + ? " — the ESP waits on its Setup/Apps providers without any timeout" + : " — device preparation waits on its providers until their timeout"); _post.Emit(new EnrollmentEvent { @@ -298,6 +316,13 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Enrollment.SystemSignals /// Non-null when the sidecar-missing condition crossed the dwell threshold this round and was not yet reported (arm 2). public double? SidecarMissingStalledForMinutes { get; set; } + + /// The phase gate of this round; the cross-restart fingerprint applies the same filter. + public bool DevicePreparationOpen { get; set; } + + /// Whether arm 1 considers in this round. + public bool Counts(PolicyProviderState provider) + => DevicePreparationOpen || provider.Kind != EspPolicyProviderProbe.KindDevicePreparation; } internal readonly struct StalledProvider diff --git a/src/Backend/AutopilotMonitor.Functions.Tests/DiagnosisHeadlineBuilderTests.cs b/src/Backend/AutopilotMonitor.Functions.Tests/DiagnosisHeadlineBuilderTests.cs index 9634605a..8c6aaeb1 100644 --- a/src/Backend/AutopilotMonitor.Functions.Tests/DiagnosisHeadlineBuilderTests.cs +++ b/src/Backend/AutopilotMonitor.Functions.Tests/DiagnosisHeadlineBuilderTests.cs @@ -365,6 +365,51 @@ public class DiagnosisHeadlineBuilderTests Assert.Equal(DiagnosisCauses.EspStepFailure, h.Cause); } + [Fact] + public void A_device_preparation_provider_is_no_culprit_for_a_later_apps_failure() + { + // A ConfigMgr client installed as a Win32 app registers its device-preparation provider + // after device preparation ended; its stall explains no device-setup failure. + var events = new Timeline() + .Add("esp_policy_provider_stalled", new() + { + ["reason"] = "provider_incomplete", + ["stalledProviders"] = new List + { + new Dictionary { ["name"] = "ConfigMgr", ["scope"] = "device", ["kind"] = "devicePreparation", ["stalledForMinutes"] = 15.0 }, + }, + }) + .Add("enrollment_failed", Failed("DeviceSetup", "Apps")); + + var h = DiagnosisHeadlineBuilder.Build(Session(SessionStatus.Failed, VerdictPaths.AgentFailed), events.Events); + + Assert.Null(h.Culprits); + } + + [Fact] + public void Only_device_preparation_providers_explain_a_device_preparation_failure() + { + // Field shape: the ESP never left device preparation (0x800705b4). + var events = new Timeline() + .Add("esp_policy_provider_stalled", new() + { + ["reason"] = "sidecar_provider_missing", + ["stalledProviders"] = new List + { + new Dictionary { ["name"] = "ConfigMgr", ["scope"] = "device", ["kind"] = "devicePreparation", ["stalledForMinutes"] = 40.0 }, + new Dictionary { ["name"] = "Contoso", ["scope"] = "device", ["kind"] = "setupApps", ["stalledForMinutes"] = 40.0 }, + }, + }) + .Add("enrollment_failed", Failed("DevicePreparation", "EspProviderInstallation", "0x800705b4")); + + var h = DiagnosisHeadlineBuilder.Build(Session(SessionStatus.Failed, VerdictPaths.AgentFailed), events.Events); + + var provider = Assert.Single(h.Culprits!); + Assert.Equal(DiagnosisCulpritKinds.PolicyProvider, provider.Kind); + Assert.Equal("ConfigMgr", provider.Name); + Assert.Equal(40.0, provider.StalledForMinutes); + } + [Fact] public void The_appx_log_candidate_joins_the_culprits() { diff --git a/src/Backend/AutopilotMonitor.Functions/Services/Diagnosis/DiagnosisCulpritFinder.cs b/src/Backend/AutopilotMonitor.Functions/Services/Diagnosis/DiagnosisCulpritFinder.cs index 7cfc549b..078b2a3d 100644 --- a/src/Backend/AutopilotMonitor.Functions/Services/Diagnosis/DiagnosisCulpritFinder.cs +++ b/src/Backend/AutopilotMonitor.Functions/Services/Diagnosis/DiagnosisCulpritFinder.cs @@ -18,6 +18,10 @@ internal static class DiagnosisCulpritFinder private static readonly IReadOnlyList EvidenceRank = DiagnosisCulpritEvidence.All; + // The agent's provider kinds (EspPolicyProviderProbe.KindDevicePreparation / KindSetupApps). + private const string DevicePreparationProviderKind = "devicePreparation"; + private const string SetupAppsProviderKind = "setupApps"; + internal static List Find( IReadOnlyList events, FailureEvidence failure, EspBlockingSets? blocking) { @@ -89,7 +93,7 @@ internal static class DiagnosisCulpritFinder } } - foreach (var provider in StalledProviders(events)) + foreach (var provider in StalledProviders(events, failure.Step.Category)) Add(provider); return culprits @@ -125,14 +129,21 @@ internal static class DiagnosisCulpritFinder } } - /// Stuck ESP policy providers from the latest esp_policy_provider_stalled. - internal static IEnumerable StalledProviders(IReadOnlyList events) + /// + /// Stuck ESP policy providers from the latest esp_policy_provider_stalled. Given the + /// failed ESP category, only providers that can hold it count: device-preparation providers + /// for a device-preparation failure, Setup/Apps providers for device or account setup. A + /// ConfigMgr client installed as a Win32 app registers its device-preparation provider after + /// device preparation ended, so that provider explains no later failure. + /// + internal static IEnumerable StalledProviders(IReadOnlyList events, string? failedCategory = null) { var stalled = DiagnosisEspEvidence.LastOf(events, e => e.EventType == Constants.EventTypes.EspPolicyProviderStalled); foreach (var item in EventDataReader.Maps(stalled?.Data, "stalledProviders")) { var name = DiagnosisEspEvidence.NonEmpty(EventDataReader.String(item, "name")); if (name == null) continue; + if (!CanHoldCategory(EventDataReader.String(item, "kind"), failedCategory)) continue; yield return new DiagnosisCulprit { Kind = DiagnosisCulpritKinds.PolicyProvider, @@ -146,6 +157,15 @@ internal static class DiagnosisCulpritFinder } } + private static bool CanHoldCategory(string? kind, string? failedCategory) + { + if (string.IsNullOrEmpty(failedCategory) || string.IsNullOrEmpty(kind)) return true; + var devicePreparationFailed = string.Equals(failedCategory, "DevicePreparation", StringComparison.OrdinalIgnoreCase); + if (string.Equals(kind, DevicePreparationProviderKind, StringComparison.OrdinalIgnoreCase)) return devicePreparationFailed; + if (string.Equals(kind, SetupAppsProviderKind, StringComparison.OrdinalIgnoreCase)) return !devicePreparationFailed; + return true; + } + private static DiagnosisCulprit AppCulprit(AppTrack app, string evidence, EspBlockingSets? blocking, string? eventId) => new() { Kind = DiagnosisCulpritKinds.App, diff --git a/src/Shared/AutopilotMonitor.Shared/Constants.cs b/src/Shared/AutopilotMonitor.Shared/Constants.cs index dcdcab81..41fcc72d 100644 --- a/src/Shared/AutopilotMonitor.Shared/Constants.cs +++ b/src/Shared/AutopilotMonitor.Shared/Constants.cs @@ -741,7 +741,7 @@ namespace AutopilotMonitor.Shared public const string TelemetryUploadBlocked = "telemetry_upload_blocked"; // Warning (one-shot each) when the drain is blocked by a retained unknown permanent 4xx (400 contract bug / 404 route mismatch / tenant-validation), or by an interception on the network path (data.reason client_certificate_required | foreign_response, plus operation, statusCode, pageTitle, serverCertIssuer, firstBlockedAtUtc). Data is NOT discarded. LOCAL diagnostic (agent log + diag ZIP): this marker is queued behind the blocking batch and only reaches the backend after the block clears — no out-of-band bypass by design. public const string SessionParkedWithoutDeadline = "session_parked_without_deadline"; // One-shot Warning tripwire (liveness plan PR1): a non-terminal session entered the post-AccountSetup dead-end zone (ESP final exit or advisory failure) with NO resolution-capable deadline armed. After the 2026-06-12 dead-end fixes this must never fire in production — every occurrence is a bug report for an unknown parking variant. public const string DiskSpaceLow = "disk_space_low"; // One-shot Warning (state-change-only, 1 GB hysteresis — NOT periodic) when the system drive drops below 2 GB free. Review MON-B10: disk-full was previously only visible inside Debug performance_snapshot payloads (filtered out of timelines), so a disk-starved enrollment carried no actionable threshold signal. - public const string EspPolicyProviderStalled = "esp_policy_provider_stalled"; // One-shot-per-provider Warning: a registered EnrollmentStatusTracking CSP policy provider stayed incomplete >= 15 min (Setup/Apps TrackingPoliciesCreated != 1, or DevicePreparation InstallationState not in {2,3}). The ESP waits on Setup/Apps providers WITHOUT any OS timeout — e.g. pre-provisioning + co-management leaves only "ConfigMgr" registered (no "Sidecar" = IME) and the user ESP sits at "Apps (Identifying)" for days. Observational only; payload carries the full provider list. See EspPolicyProviderProbe. + public const string EspPolicyProviderStalled = "esp_policy_provider_stalled"; // One-shot-per-provider Warning: a registered EnrollmentStatusTracking CSP policy provider stayed incomplete >= 15 min (Setup/Apps TrackingPoliciesCreated != 1, or DevicePreparation InstallationState not in {2,3} while device preparation is still open). The ESP waits on Setup/Apps providers WITHOUT any OS timeout — e.g. pre-provisioning + co-management leaves only "ConfigMgr" registered (no "Sidecar" = IME) and the user ESP sits at "Apps (Identifying)" for days. Observational only; payload carries the full provider list. See EspPolicyProviderProbe. public const string KeepAwakeEngaged = "keep_awake_engaged"; // Info (state-change, NOT periodic) emitted by the opt-in UserEspKeepAwake host when it holds the device awake (system + display) on entering the User-ESP/AccountSetup phase, so idle standby cannot stall app installs / account provisioning. Reboots are unaffected. Per-tenant toggle (AnalyzerConfiguration.KeepAwakeDuringUserEsp, default off). public const string KeepAwakeReleased = "keep_awake_released"; // Info (Warning when reason=safety_cap) emitted when the keep-awake hold is released — reason = user_esp_exited (Shell-Core 62407 after AccountSetup progress, i.e. the user-ESP page closed; agents before 2026-09-04 wrote account_setup_complete for the synthesized completion, which fired minutes early) | host_stop | safety_cap. Brackets KeepAwakeEngaged; the OS also auto-clears the hold on process exit/reboot.