(utc, sourceSignalOrdinal);
+ return this;
+ }
+
public DecisionState Build() =>
new DecisionState(
sessionId: SessionId,
@@ -212,6 +226,7 @@ namespace AutopilotMonitor.DecisionCore.State
lastFailureTrigger: LastFailureTrigger,
realmJoinFacts: RealmJoinFacts,
deviceSetupResolvedUtc: DeviceSetupResolvedUtc,
- schemaVersion: SchemaVersion);
+ schemaVersion: SchemaVersion,
+ espAdvisoryFailureRecordedUtc: EspAdvisoryFailureRecordedUtc);
}
}
diff --git a/src/Shared/AutopilotMonitor.Shared/Constants.cs b/src/Shared/AutopilotMonitor.Shared/Constants.cs
index e646fd38..7032205d 100644
--- a/src/Shared/AutopilotMonitor.Shared/Constants.cs
+++ b/src/Shared/AutopilotMonitor.Shared/Constants.cs
@@ -245,6 +245,11 @@ namespace AutopilotMonitor.Shared
public const string WhiteGloveComplete = "whiteglove_complete";
public const string EnrollmentComplete = "enrollment_complete";
public const string EnrollmentFailed = "enrollment_failed";
+ // PR1 — ContinueAnyway-aware ESP terminal failure defang (Session 4fa5a2d4, 2026-05-22).
+ // Emitted instead of enrollment_failed when the ESP profile permits ContinueAnyway
+ // AND AccountSetup has already been entered — proves the device progressed past
+ // DeviceSetup so the agent stays in monitoring instead of terminating.
+ public const string EspFailureAdvisory = "esp_failure_advisory";
public const string DesktopArrived = "desktop_arrived";
// DAD liveness (state-change-only, max 3 events per detector lifetime, NOT periodic).
// Distinguishes "DAD never started after reboot" vs "DAD started but timer/WMI dead" vs
diff --git a/src/Web/autopilot-monitor-web/app/gather-rules/eventTypes.ts b/src/Web/autopilot-monitor-web/app/gather-rules/eventTypes.ts
index f01e0248..8f1801ee 100644
--- a/src/Web/autopilot-monitor-web/app/gather-rules/eventTypes.ts
+++ b/src/Web/autopilot-monitor-web/app/gather-rules/eventTypes.ts
@@ -66,6 +66,8 @@ export const KNOWN_EVENT_TYPES: EventTypeEntry[] = [
description: "ESP category status update from Windows Provisioning registry." },
{ value: "esp_state_change", label: "esp_state_change", category: "esp",
description: "Generic ESP state change event." },
+ { value: "esp_failure_advisory", label: "esp_failure_advisory", category: "esp",
+ description: "ESP reported a subcategory failure but the device had already progressed to AccountSetup with ContinueAnyway enabled — non-terminal advisory, the agent continues monitoring." },
// -------- App installs --------
{ value: "app_install_started", label: "app_install_started", category: "app",
diff --git a/src/Web/autopilot-monitor-web/app/sessions/[sessionId]/components/EventTimeline.tsx b/src/Web/autopilot-monitor-web/app/sessions/[sessionId]/components/EventTimeline.tsx
index b1d24f6d..47471280 100644
--- a/src/Web/autopilot-monitor-web/app/sessions/[sessionId]/components/EventTimeline.tsx
+++ b/src/Web/autopilot-monitor-web/app/sessions/[sessionId]/components/EventTimeline.tsx
@@ -479,22 +479,38 @@ function EventRow({ event, showScriptOutput }: { event: EnrollmentEvent; showScr
);
})()}
{/* HRESULT badge for ESP failures (enrollment_failed via esp_terminal_failure,
- esp_provisioning_status failed-subcategory). The HRESULT is extracted from the ESP
- registry statusText by the agent (e.g. "Apps (0x87d1041c)") and surfaced as
- top-level event data so the UI can render it without parsing nested text. */}
- {(event.eventType === "enrollment_failed" || event.eventType === "esp_provisioning_status") && (() => {
+ esp_provisioning_status failed-subcategory, esp_failure_advisory via ContinueAnyway
+ defang). The HRESULT is extracted from the ESP registry statusText by the agent
+ (e.g. "Apps (0x87d1041c)") and surfaced as top-level event data so the UI can
+ render it without parsing nested text. */}
+ {(event.eventType === "enrollment_failed" || event.eventType === "esp_provisioning_status" || event.eventType === "esp_failure_advisory") && (() => {
const code = event.data?.errorCode ?? event.data?.failedSubcategoryErrorCode;
- if (!code) return null;
- const codeStr = String(code);
- const entry = getErrorCodeEntry(codeStr);
const sub = event.data?.failedSubcategory ?? event.data?.failedSubcategories;
+ const isAdvisory = event.eventType === "esp_failure_advisory";
+ if (!code && !isAdvisory) return null;
+ const codeStr = code ? String(code) : null;
+ const entry = codeStr ? getErrorCodeEntry(codeStr) : null;
+ // Advisory path uses warning-color palette (amber); the device continued past the
+ // failure via ContinueAnyway, so this is not a hard error. PR1 Session 4fa5a2d4.
+ const badgeBg = isAdvisory ? "bg-amber-100 text-amber-800" : "bg-red-100 text-red-800";
+ const descColor = isAdvisory ? "text-amber-700" : "text-red-600";
return (
-
- HRESULT: {formatErrorCode(codeStr)}
-
+ {isAdvisory && (
+
+ Advisory (ContinueAnyway)
+
+ )}
+ {codeStr && (
+
+ HRESULT: {formatErrorCode(codeStr)}
+
+ )}
{entry && (
-
+
{entry.description}
)}
diff --git a/tests/fixtures/enrollment-sessions/esp-failure-advisory-continueanyway-v1.jsonl b/tests/fixtures/enrollment-sessions/esp-failure-advisory-continueanyway-v1.jsonl
new file mode 100644
index 00000000..519f5076
--- /dev/null
+++ b/tests/fixtures/enrollment-sessions/esp-failure-advisory-continueanyway-v1.jsonl
@@ -0,0 +1,22 @@
+# Scenario: ESP Terminal Failure DEFANGED by ContinueAnyway (PR1, Session 4fa5a2d4, 2026-05-22)
+# Tenant c9787ba2 observed 199/202 sessions failing with esp_terminal_failure, even though
+# customers were demonstrably deploying 1200+ devices through the same profile. Real root cause:
+# - ESP profile sets BlockInStatusPage=6 (ContinueAnyway=true)
+# - DeviceSetup completes Apps successfully
+# - Windows transitions to AccountSetup (esp_phase_changed)
+# - Then Windows writes DeviceSetup.Certificates=failed AFTER AccountSetup has already started
+# - V2 reducer fired EspTerminalFailure → Failed, even though the device kept progressing
+#
+# PR1 defang: when ContinueAnyway=true AND AccountSetupEnteredUtc is set, the reducer downgrades
+# the EspTerminalFailure to an esp_failure_advisory effect; stage stays at EspAccountSetup,
+# EspAdvisoryFailureRecordedUtc is set, monitoring continues. The session never reaches Failed
+# in this replay (it would normally complete via Desktop+Hello in production — not modeled here).
+#
+# Coverage: validates that the engine produces the advisory effect end-to-end through the same
+# signal stream the real session 4fa5a2d4 saw.
+{"SessionSignalOrdinal":0,"SessionTraceOrdinal":0,"Kind":"SessionStarted","KindSchemaVersion":1,"OccurredAtUtc":"2026-05-22T09:00:00Z","SourceOrigin":"orchestrator","Evidence":{"Kind":"Synthetic","Identifier":"session:advisory:started","Summary":"Session start anchor"}}
+{"SessionSignalOrdinal":1,"SessionTraceOrdinal":1,"Kind":"EspConfigDetected","KindSchemaVersion":1,"OccurredAtUtc":"2026-05-22T09:00:30Z","SourceOrigin":"EnrollmentOrchestrator","Evidence":{"Kind":"Raw","Identifier":"esp_config_detected_bootstrap","Summary":"SkipUser=False, SkipDevice=False, AllowContinueAnyway=True","DerivationInputs":{"source":"registry_firstsync_bootstrap"}},"Payload":{"skipUserEsp":"false","skipDeviceEsp":"false","espAllowContinueAnyway":"true","espSyncFailureTimeoutMinutes":"30"}}
+{"SessionSignalOrdinal":2,"SessionTraceOrdinal":2,"Kind":"EspPhaseChanged","KindSchemaVersion":1,"OccurredAtUtc":"2026-05-22T09:01:00Z","SourceOrigin":"EspAndHelloTracker","Evidence":{"Kind":"Derived","Identifier":"esp-phase-detector-v1","Summary":"ESP phase=DeviceSetup","DerivationInputs":{"rawValue":"1"}},"Payload":{"phase":"DeviceSetup"}}
+{"SessionSignalOrdinal":3,"SessionTraceOrdinal":3,"Kind":"EspPhaseChanged","KindSchemaVersion":1,"OccurredAtUtc":"2026-05-22T09:16:00Z","SourceOrigin":"EspAndHelloTracker","Evidence":{"Kind":"Derived","Identifier":"esp-phase-detector-v1","Summary":"ESP phase=AccountSetup","DerivationInputs":{"rawValue":"2"}},"Payload":{"phase":"AccountSetup"}}
+{"SessionSignalOrdinal":4,"SessionTraceOrdinal":4,"Kind":"EspTerminalFailure","KindSchemaVersion":1,"OccurredAtUtc":"2026-05-22T09:29:52Z","SourceOrigin":"ProvisioningStatusTracker","Evidence":{"Kind":"Derived","Identifier":"provisioning-status-tracker-v1","Summary":"ESP terminal failure from provisioning registry (type=Provisioning_DeviceSetup_Certificates_Failed, errorCode=n/a)","DerivationInputs":{"registrySource":"HKLM\\SOFTWARE\\Microsoft\\Provisioning\\AutopilotSettings\\*.Status","failureType":"Provisioning_DeviceSetup_Certificates_Failed","failedSubcategory":"Certificates","category":"DeviceSetup"}},"Payload":{"failureType":"Provisioning_DeviceSetup_Certificates_Failed","failedSubcategory":"Certificates","category":"DeviceSetup"}}
+{"SessionSignalOrdinal":5,"SessionTraceOrdinal":5,"Kind":"EspTerminalFailure","KindSchemaVersion":1,"OccurredAtUtc":"2026-05-22T09:30:15Z","SourceOrigin":"ShellCoreTracker","Evidence":{"Kind":"Raw","Identifier":"eventlog:Shell-Core:62411:recordId:1234","Summary":"Duplicate ESP terminal failure (Event 62411) — must dead-end after advisory","RawPointer":"blob://events/shell-core/1234"},"Payload":{"reason":"esp_terminal_failure_account_setup"}}