mirror of
https://github.com/okieselbach/AutopilotMonitor.git
synced 2026-10-24 20:53:06 +00:00
fix(backend): reconcile silent sessions to Succeeded when user provably finished setup (session 294ab5b4)
The timeout classifier only read the ESP registry rollup, so a session where the user reached the desktop and completed Windows Hello — with only the RealmJoin completion gate still pending when the agent went silent — was labeled AwaitingUser (and would have graduated to Incomplete at grace expiry). Desktop arrival + a positive Hello terminal are the agent's own Classic completion AND-gate; the RJ gate self-releases after 60 min, so such a session's enrollment succeeded and only the completion report never left the device. - EnrollmentTimeoutClassifier: rollup now carries DesktopArrived, HelloResolved (hello_provisioning_completed/hello_skipped only) and RealmJoinDetected/Resolved (realmjoin_resolved/realmjoin_timeout); new rule 4 reconciles desktop+Hello sessions to Succeeded with an RJ-aware reason. Explicit failure and agent_emergency_break still win. - FailureSnapshotBuilder: schema v3 embeds the new evidence (helloResolved, realmJoinDetected, realmJoinResolved). - Web FailureSnapshotBlock: renders Hello completed + RealmJoin rows. - Design doc: decision table updated; desktop_arrived ALONE stays rejected as a completion signal. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -100,13 +100,28 @@ Given a non-terminal session past the inactivity window:
|
|||||||
1. Explicit enrollment_failed / esp_failure present → Failed (real)
|
1. Explicit enrollment_failed / esp_failure present → Failed (real)
|
||||||
2. AccountSetup rollup = ALL subcategories succeeded
|
2. AccountSetup rollup = ALL subcategories succeeded
|
||||||
OR enrollment_complete / whiteglove_complete present → Succeeded (reconcile)
|
OR enrollment_complete / whiteglove_complete present → Succeeded (reconcile)
|
||||||
3. DeviceSetup rollup = ALL subcategories succeeded (or 30s
|
3. agent_emergency_break present (agent provably gone) → Incomplete (skip grace)
|
||||||
|
4. desktop_arrived AND positive Hello terminal
|
||||||
|
(hello_provisioning_completed / hello_skipped) present → Succeeded (reconcile)
|
||||||
|
5. DeviceSetup rollup = ALL subcategories succeeded (or 30s
|
||||||
fallback) AND no failure event:
|
fallback) AND no failure event:
|
||||||
a. within grace (now - StartedAt < SessionGraceHours) → AwaitingUser (non-terminal)
|
a. within grace (now - StartedAt < SessionGraceHours) → AwaitingUser (non-terminal)
|
||||||
b. grace expired → Incomplete (terminal, non-failure)
|
b. grace expired → Incomplete (terminal, non-failure)
|
||||||
4. else (silence before DeviceSetup complete, no failure) → Incomplete
|
6. else (silence before DeviceSetup complete, no failure) → Incomplete
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Step 4 (added 2026-07-10, trigger session `294ab5b4`): desktop arrival **plus** a positive
|
||||||
|
Hello terminal is exactly the agent's own Classic completion AND‑gate — once both are in,
|
||||||
|
the only thing that can still block the agent's `enrollment_complete` is the RealmJoin
|
||||||
|
completion gate, which self‑releases via a 60‑min hard timeout. A session silent long
|
||||||
|
enough to reach the sweep is past every agent‑side wait: the enrollment succeeded and only
|
||||||
|
the completion report never left the device (shutdown / process kill / egress cut).
|
||||||
|
Labeling it `AwaitingUser` is factually wrong — the user was provably there. The reason
|
||||||
|
string distinguishes "RealmJoin deployment never reported completion" (RJ detected but
|
||||||
|
unresolved) from the generic "agent went silent before reporting completion". The failure
|
||||||
|
snapshot (schema v3) carries the evidence: `helloResolved`, `realmJoinDetected`,
|
||||||
|
`realmJoinResolved`.
|
||||||
|
|
||||||
Rationale for each bucket sizing is in the Evidence table above.
|
Rationale for each bucket sizing is in the Evidence table above.
|
||||||
⚠️ Gate step 2/3 on the **subcategory rollup**, not `categorySucceeded`.
|
⚠️ Gate step 2/3 on the **subcategory rollup**, not `categorySucceeded`.
|
||||||
|
|
||||||
@@ -197,7 +212,9 @@ Platform‑wide the same reclassification de‑noises every tenant's failure rat
|
|||||||
- **Agent heartbeat** — rejected: continuous client load + zombie risk; the completion
|
- **Agent heartbeat** — rejected: continuous client load + zombie risk; the completion
|
||||||
truth is already persisted (registry/ESP tracking survives reboots) and the wait belongs
|
truth is already persisted (registry/ESP tracking survives reboots) and the wait belongs
|
||||||
in the backend.
|
in the backend.
|
||||||
- **`desktop_arrived` as completion** — rejected: fires while the user phase is still running.
|
- **`desktop_arrived` ALONE as completion** — rejected: fires while the user phase is still
|
||||||
|
running. (`desktop_arrived` **+ a positive Hello terminal** is accepted since 2026-07-10 —
|
||||||
|
that pair is the agent's own Classic completion AND‑gate, see classification step 4.)
|
||||||
|
|
||||||
## Acceptance criteria
|
## Acceptance criteria
|
||||||
|
|
||||||
|
|||||||
@@ -88,6 +88,28 @@ public class EnrollmentTimeoutClassifierTests
|
|||||||
Assert.True(EnrollmentTimeoutClassifier.ExtractRollup(new[] { Evt("agent_emergency_break") }).HasAgentEmergencyBreak);
|
Assert.True(EnrollmentTimeoutClassifier.ExtractRollup(new[] { Evt("agent_emergency_break") }).HasAgentEmergencyBreak);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ExtractRollup_detects_desktop_hello_and_realmjoin_evidence()
|
||||||
|
{
|
||||||
|
var r = EnrollmentTimeoutClassifier.ExtractRollup(new[]
|
||||||
|
{
|
||||||
|
Evt("desktop_arrived"), Evt("hello_provisioning_completed"), Evt("realmjoin_detected"),
|
||||||
|
});
|
||||||
|
Assert.True(r.DesktopArrived);
|
||||||
|
Assert.True(r.HelloResolved);
|
||||||
|
Assert.True(r.RealmJoinDetected);
|
||||||
|
Assert.False(r.RealmJoinResolved);
|
||||||
|
|
||||||
|
// hello_skipped is the other positive Hello terminal (agent raises HelloResolved for it).
|
||||||
|
Assert.True(EnrollmentTimeoutClassifier.ExtractRollup(new[] { Evt("hello_skipped") }).HelloResolved);
|
||||||
|
// Negative Hello terminals leave the agent waiting — must NOT count as resolved.
|
||||||
|
Assert.False(EnrollmentTimeoutClassifier.ExtractRollup(new[] { Evt("hello_provisioning_failed") }).HelloResolved);
|
||||||
|
Assert.False(EnrollmentTimeoutClassifier.ExtractRollup(new[] { Evt("hello_completion_timeout") }).HelloResolved);
|
||||||
|
// Both RealmJoin gate terminals count as resolved (phase 110 or 60-min hard timeout).
|
||||||
|
Assert.True(EnrollmentTimeoutClassifier.ExtractRollup(new[] { Evt("realmjoin_resolved") }).RealmJoinResolved);
|
||||||
|
Assert.True(EnrollmentTimeoutClassifier.ExtractRollup(new[] { Evt("realmjoin_timeout") }).RealmJoinResolved);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Classify_emergency_break_skips_grace_and_is_Incomplete()
|
public void Classify_emergency_break_skips_grace_and_is_Incomplete()
|
||||||
{
|
{
|
||||||
@@ -138,6 +160,84 @@ public class EnrollmentTimeoutClassifierTests
|
|||||||
Assert.Equal(SessionStatus.Succeeded, status);
|
Assert.Equal(SessionStatus.Succeeded, status);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// -------- "user completed setup" reconcile (session 294ab5b4) --------
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Classify_desktop_plus_hello_with_unresolved_realmjoin_reconciles_to_Succeeded()
|
||||||
|
{
|
||||||
|
// Session 294ab5b4 replay: DeviceSetup 4/4, AccountSetup frozen at 1/5 after the user
|
||||||
|
// hit the desktop, Hello provisioned, RealmJoin detected but never resolved — agent
|
||||||
|
// went silent mid-deployment. The user was provably there; "AwaitingUser" is wrong.
|
||||||
|
var (status, reason) = Classify(new[]
|
||||||
|
{
|
||||||
|
Esp(DeviceSetup44), Esp(AccountSetup15),
|
||||||
|
Evt("desktop_arrived"), Evt("hello_provisioning_completed"), Evt("realmjoin_detected"),
|
||||||
|
}, hoursSinceStart: 6);
|
||||||
|
Assert.Equal(SessionStatus.Succeeded, status);
|
||||||
|
Assert.Contains("RealmJoin", reason);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Classify_desktop_plus_hello_without_realmjoin_reconciles_to_Succeeded()
|
||||||
|
{
|
||||||
|
// Both Classic completion prerequisites in and no gate pending: the agent died in the
|
||||||
|
// narrow window before it could report enrollment_complete.
|
||||||
|
var (status, reason) = Classify(new[]
|
||||||
|
{
|
||||||
|
Esp(DeviceSetup44), Esp(AccountSetup15),
|
||||||
|
Evt("desktop_arrived"), Evt("hello_provisioning_completed"),
|
||||||
|
}, hoursSinceStart: 6);
|
||||||
|
Assert.Equal(SessionStatus.Succeeded, status);
|
||||||
|
Assert.Contains("silent before reporting completion", reason);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Classify_desktop_plus_hello_skipped_also_reconciles_to_Succeeded()
|
||||||
|
{
|
||||||
|
var (status, _) = Classify(new[]
|
||||||
|
{
|
||||||
|
Esp(DeviceSetup44), Evt("desktop_arrived"), Evt("hello_skipped"),
|
||||||
|
}, hoursSinceStart: 6);
|
||||||
|
Assert.Equal(SessionStatus.Succeeded, status);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Classify_desktop_without_hello_terminal_stays_AwaitingUser()
|
||||||
|
{
|
||||||
|
// desktop_arrived alone is explicitly NOT a completion signal (design doc) — the user
|
||||||
|
// may still be mid Hello wizard / user phase. Falls through to the AwaitingUser rule.
|
||||||
|
var (status, _) = Classify(new[]
|
||||||
|
{
|
||||||
|
Esp(DeviceSetup44), Esp(AccountSetup15), Evt("desktop_arrived"),
|
||||||
|
}, hoursSinceStart: 6);
|
||||||
|
Assert.Equal(SessionStatus.AwaitingUser, status);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Classify_explicit_failure_beats_desktop_plus_hello()
|
||||||
|
{
|
||||||
|
var (status, _) = Classify(new[]
|
||||||
|
{
|
||||||
|
Esp(DeviceSetup44), Evt("desktop_arrived"), Evt("hello_provisioning_completed"),
|
||||||
|
Evt("enrollment_failed"),
|
||||||
|
}, hoursSinceStart: 6);
|
||||||
|
Assert.Equal(SessionStatus.Failed, status);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Classify_emergency_break_beats_desktop_plus_hello()
|
||||||
|
{
|
||||||
|
// The break means the agent stayed alive to the 48h absolute cap WITHOUT completing —
|
||||||
|
// despite both prerequisites being in, something blocked completion for two days.
|
||||||
|
// That is not a success; the honest verdict stays Incomplete.
|
||||||
|
var (status, _) = Classify(new[]
|
||||||
|
{
|
||||||
|
Esp(DeviceSetup44), Evt("desktop_arrived"), Evt("hello_provisioning_completed"),
|
||||||
|
Evt("agent_emergency_break"),
|
||||||
|
}, hoursSinceStart: 50);
|
||||||
|
Assert.Equal(SessionStatus.Incomplete, status);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Classify_device_provisioned_user_phase_pending_within_grace_is_AwaitingUser()
|
public void Classify_device_provisioned_user_phase_pending_within_grace_is_AwaitingUser()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -311,6 +311,25 @@ public class FailureSnapshotBuilderTests
|
|||||||
Assert.DoesNotContain("hello_terminal", missing);
|
Assert.DoesNotContain("hello_terminal", missing);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Build_records_user_completion_evidence_for_reconcile_rule()
|
||||||
|
{
|
||||||
|
// Schema v3 (session 294ab5b4): helloResolved + realmJoin* are the evidence behind the
|
||||||
|
// "user completed setup" reconcile verdict — the snapshot must show operators the same
|
||||||
|
// facts the classifier used.
|
||||||
|
var json = FailureSnapshotBuilder.Build(new[]
|
||||||
|
{
|
||||||
|
Event("desktop_arrived", Now.AddHours(-5)),
|
||||||
|
Event("hello_provisioning_completed", Now.AddHours(-5).AddMinutes(6)),
|
||||||
|
Event("realmjoin_detected", Now.AddHours(-5).AddMinutes(1)),
|
||||||
|
}, Now);
|
||||||
|
|
||||||
|
var obj = JObject.Parse(json!);
|
||||||
|
Assert.True((bool)obj["helloResolved"]!);
|
||||||
|
Assert.True((bool)obj["realmJoinDetected"]!);
|
||||||
|
Assert.False((bool)obj["realmJoinResolved"]!);
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Timing / stale silence
|
// Timing / stale silence
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|||||||
@@ -78,7 +78,11 @@ namespace AutopilotMonitor.Functions.Services
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Distilled ESP lifecycle facts extracted from a session's event stream — the inputs
|
/// Distilled ESP lifecycle facts extracted from a session's event stream — the inputs
|
||||||
/// to <see cref="ClassifyTimedOutSession"/>. Also embedded into the failure snapshot so
|
/// to <see cref="ClassifyTimedOutSession"/>. Also embedded into the failure snapshot so
|
||||||
/// operators see the same evidence the classifier used.
|
/// operators see the same evidence the classifier used. Besides the ESP registry rollup
|
||||||
|
/// this carries the user-presence evidence (desktop arrival + positive Hello terminal —
|
||||||
|
/// the agent's Classic completion prerequisites) and the RealmJoin gate state, so the
|
||||||
|
/// classifier can tell "user was provably there" apart from "awaiting user" (session
|
||||||
|
/// 294ab5b4).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public readonly record struct EspProvisioningRollup(
|
public readonly record struct EspProvisioningRollup(
|
||||||
bool DeviceSetupAllSucceeded,
|
bool DeviceSetupAllSucceeded,
|
||||||
@@ -87,7 +91,11 @@ namespace AutopilotMonitor.Functions.Services
|
|||||||
bool AccountSetupAllSucceeded,
|
bool AccountSetupAllSucceeded,
|
||||||
bool HasExplicitFailure,
|
bool HasExplicitFailure,
|
||||||
bool HasTerminalComplete,
|
bool HasTerminalComplete,
|
||||||
bool HasAgentEmergencyBreak);
|
bool HasAgentEmergencyBreak,
|
||||||
|
bool DesktopArrived,
|
||||||
|
bool HelloResolved,
|
||||||
|
bool RealmJoinDetected,
|
||||||
|
bool RealmJoinResolved);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Walk a session's events and distill the ESP rollup. Tolerant of missing/empty input
|
/// Walk a session's events and distill the ESP rollup. Tolerant of missing/empty input
|
||||||
@@ -103,6 +111,8 @@ namespace AutopilotMonitor.Functions.Services
|
|||||||
int acctBestN = 0, acctBestM = 0;
|
int acctBestN = 0, acctBestM = 0;
|
||||||
bool acctFallbackAll = false;
|
bool acctFallbackAll = false;
|
||||||
bool hasFailure = false, hasComplete = false, hasEmergencyBreak = false;
|
bool hasFailure = false, hasComplete = false, hasEmergencyBreak = false;
|
||||||
|
bool desktopArrived = false, helloResolved = false;
|
||||||
|
bool realmJoinDetected = false, realmJoinResolved = false;
|
||||||
|
|
||||||
foreach (var evt in events)
|
foreach (var evt in events)
|
||||||
{
|
{
|
||||||
@@ -114,6 +124,19 @@ namespace AutopilotMonitor.Functions.Services
|
|||||||
hasComplete = true;
|
hasComplete = true;
|
||||||
else if (Eq(type, "agent_emergency_break"))
|
else if (Eq(type, "agent_emergency_break"))
|
||||||
hasEmergencyBreak = true;
|
hasEmergencyBreak = true;
|
||||||
|
else if (Eq(type, "desktop_arrived"))
|
||||||
|
desktopArrived = true;
|
||||||
|
// Positive Hello terminals only (provisioned / skipped) — these are what raises
|
||||||
|
// the agent's HelloResolved completion fact. _failed / _blocked / _timeout leave
|
||||||
|
// the agent still waiting and must not count as "user finished setup".
|
||||||
|
else if (Eq(type, "hello_provisioning_completed") || Eq(type, "hello_skipped"))
|
||||||
|
helloResolved = true;
|
||||||
|
else if (Eq(type, "realmjoin_detected"))
|
||||||
|
realmJoinDetected = true;
|
||||||
|
// Either terminal opens the agent-side RealmJoin gate: phase 110 or the 60-min
|
||||||
|
// hard timeout (both dual-emitted to the timeline by the agent/engine).
|
||||||
|
else if (Eq(type, "realmjoin_resolved") || Eq(type, "realmjoin_timeout"))
|
||||||
|
realmJoinResolved = true;
|
||||||
|
|
||||||
var msg = evt.Message;
|
var msg = evt.Message;
|
||||||
if (string.IsNullOrEmpty(msg)) continue;
|
if (string.IsNullOrEmpty(msg)) continue;
|
||||||
@@ -152,7 +175,11 @@ namespace AutopilotMonitor.Functions.Services
|
|||||||
AccountSetupAllSucceeded: acctAll,
|
AccountSetupAllSucceeded: acctAll,
|
||||||
HasExplicitFailure: hasFailure,
|
HasExplicitFailure: hasFailure,
|
||||||
HasTerminalComplete: hasComplete,
|
HasTerminalComplete: hasComplete,
|
||||||
HasAgentEmergencyBreak: hasEmergencyBreak);
|
HasAgentEmergencyBreak: hasEmergencyBreak,
|
||||||
|
DesktopArrived: desktopArrived,
|
||||||
|
HelloResolved: helloResolved,
|
||||||
|
RealmJoinDetected: realmJoinDetected,
|
||||||
|
RealmJoinResolved: realmJoinResolved);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -184,7 +211,29 @@ namespace AutopilotMonitor.Functions.Services
|
|||||||
return (SessionStatus.Incomplete,
|
return (SessionStatus.Incomplete,
|
||||||
"Agent emergency break fired (absolute session-age cap) — agent gone without completion");
|
"Agent emergency break fired (absolute session-age cap) — agent gone without completion");
|
||||||
|
|
||||||
// 4. Device Setup fully provisioned (device is AADJ + MDM-enrolled), user phase pending.
|
// 4. The user demonstrably finished setup: a real-user desktop was observed AND
|
||||||
|
// Windows Hello reached a positive terminal (provisioned or skipped). Those are
|
||||||
|
// exactly the agent's Classic completion prerequisites — the only thing that can
|
||||||
|
// still block enrollment_complete on the device is the RealmJoin completion gate,
|
||||||
|
// and that self-releases via a 60-min hard timeout. A session still silent by the
|
||||||
|
// time this sweep runs is therefore past every agent-side wait: the enrollment
|
||||||
|
// itself succeeded and only the final completion report never left the device
|
||||||
|
// (shutdown / process kill / egress cut — session 294ab5b4). Labeling this
|
||||||
|
// "AwaitingUser" would be factually wrong (the user was provably there), so
|
||||||
|
// reconcile to Succeeded with the honest reason. Note desktop arrival ALONE
|
||||||
|
// remains explicitly rejected as a completion signal (design doc) — it fires
|
||||||
|
// while the user phase is still running; the Hello terminal is what proves the
|
||||||
|
// user finished.
|
||||||
|
if (rollup.DesktopArrived && rollup.HelloResolved)
|
||||||
|
{
|
||||||
|
var detail = rollup.RealmJoinDetected && !rollup.RealmJoinResolved
|
||||||
|
? "RealmJoin deployment never reported completion before the agent went silent"
|
||||||
|
: "agent went silent before reporting completion";
|
||||||
|
return (SessionStatus.Succeeded,
|
||||||
|
$"Reconciled at timeout: user completed setup (desktop + Windows Hello) — {detail}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Device Setup fully provisioned (device is AADJ + MDM-enrolled), user phase pending.
|
||||||
if (rollup.DeviceSetupAllSucceeded)
|
if (rollup.DeviceSetupAllSucceeded)
|
||||||
{
|
{
|
||||||
var elapsedHours = (nowUtc - startedAtUtc).TotalHours;
|
var elapsedHours = (nowUtc - startedAtUtc).TotalHours;
|
||||||
@@ -202,7 +251,7 @@ namespace AutopilotMonitor.Functions.Services
|
|||||||
$"(last Account Setup {rollup.AccountSetupSucceededCount}/{rollup.AccountSetupTotal})");
|
$"(last Account Setup {rollup.AccountSetupSucceededCount}/{rollup.AccountSetupTotal})");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. Silent before Device Setup completed, with no explicit failure → unknown, not a failure.
|
// 6. Silent before Device Setup completed, with no explicit failure → unknown, not a failure.
|
||||||
return (SessionStatus.Incomplete,
|
return (SessionStatus.Incomplete,
|
||||||
"No Device Setup completion or explicit failure signal observed before timeout");
|
"No Device Setup completion or explicit failure signal observed before timeout");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,7 +27,11 @@ namespace AutopilotMonitor.Functions.Services
|
|||||||
// v2 (2026-07-08): added the ESP DeviceSetup/AccountSetup subcategory rollup fields
|
// v2 (2026-07-08): added the ESP DeviceSetup/AccountSetup subcategory rollup fields
|
||||||
// (deviceSetupAllSucceeded / accountSetup*) that the timeout reclassification uses —
|
// (deviceSetupAllSucceeded / accountSetup*) that the timeout reclassification uses —
|
||||||
// see docs/design/enrollment-status-reclassification.md.
|
// see docs/design/enrollment-status-reclassification.md.
|
||||||
public const int CurrentSchemaVersion = 2;
|
// v3 (2026-07-10): added helloResolved + realmJoinDetected/realmJoinResolved — the
|
||||||
|
// evidence behind the "user completed setup" reconcile rule (session 294ab5b4: user
|
||||||
|
// reached desktop + finished Hello, only the RealmJoin gate was pending when the
|
||||||
|
// agent went silent; previously mislabeled AwaitingUser).
|
||||||
|
public const int CurrentSchemaVersion = 3;
|
||||||
|
|
||||||
// Canonical signals we expect to have seen by the time a healthy session completes.
|
// Canonical signals we expect to have seen by the time a healthy session completes.
|
||||||
// Missing entries are surfaced in the snapshot under "missingSignals". Some entries
|
// Missing entries are surfaced in the snapshot under "missingSignals". Some entries
|
||||||
@@ -236,6 +240,9 @@ namespace AutopilotMonitor.Functions.Services
|
|||||||
accountSetupSucceededCount = espRollup.AccountSetupSucceededCount,
|
accountSetupSucceededCount = espRollup.AccountSetupSucceededCount,
|
||||||
accountSetupTotal = espRollup.AccountSetupTotal,
|
accountSetupTotal = espRollup.AccountSetupTotal,
|
||||||
accountSetupAllSucceeded = espRollup.AccountSetupAllSucceeded,
|
accountSetupAllSucceeded = espRollup.AccountSetupAllSucceeded,
|
||||||
|
helloResolved = espRollup.HelloResolved,
|
||||||
|
realmJoinDetected = espRollup.RealmJoinDetected,
|
||||||
|
realmJoinResolved = espRollup.RealmJoinResolved,
|
||||||
missingSignals = missing,
|
missingSignals = missing,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -369,6 +369,8 @@ namespace AutopilotMonitor.Functions.Services
|
|||||||
// emits instead of blindly failing every silent session
|
// emits instead of blindly failing every silent session
|
||||||
// (docs/design/enrollment-status-reclassification.md): explicit failure → Failed;
|
// (docs/design/enrollment-status-reclassification.md): explicit failure → Failed;
|
||||||
// Account Setup all-succeeded / enrollment_complete → Succeeded (reconcile);
|
// Account Setup all-succeeded / enrollment_complete → Succeeded (reconcile);
|
||||||
|
// desktop + positive Hello terminal observed → Succeeded (user provably finished
|
||||||
|
// setup; only the completion report never left the device — session 294ab5b4);
|
||||||
// Device Setup done + within grace → AwaitingUser, else Incomplete; silent before
|
// Device Setup done + within grace → AwaitingUser, else Incomplete; silent before
|
||||||
// Device Setup → Incomplete. Never Failed without an explicit failure signal.
|
// Device Setup → Incomplete. Never Failed without an explicit failure signal.
|
||||||
var rollup = EnrollmentTimeoutClassifier.ExtractRollup(sessionEvents);
|
var rollup = EnrollmentTimeoutClassifier.ExtractRollup(sessionEvents);
|
||||||
|
|||||||
+18
@@ -27,6 +27,10 @@ interface FailureSnapshot {
|
|||||||
isHybridJoin?: boolean | null;
|
isHybridJoin?: boolean | null;
|
||||||
enrollmentType?: string | null;
|
enrollmentType?: string | null;
|
||||||
lastNetworkState?: string | null;
|
lastNetworkState?: string | null;
|
||||||
|
// Schema v3 (2026-07-10): evidence behind the "user completed setup" reconcile rule.
|
||||||
|
helloResolved?: boolean;
|
||||||
|
realmJoinDetected?: boolean;
|
||||||
|
realmJoinResolved?: boolean;
|
||||||
missingSignals?: string[];
|
missingSignals?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,6 +84,12 @@ export default function FailureSnapshotBlock({
|
|||||||
<SnapshotItem label="Last ESP phase" value={snapshot.lastEspPhase ?? "—"} sub={snapshot.lastEspPhaseAtUtc ?? undefined} />
|
<SnapshotItem label="Last ESP phase" value={snapshot.lastEspPhase ?? "—"} sub={snapshot.lastEspPhaseAtUtc ?? undefined} />
|
||||||
<SnapshotItem label="Desktop arrived" value={fmtBool(snapshot.desktopArrived)} sub={snapshot.desktopArrivedAtUtc ?? undefined} />
|
<SnapshotItem label="Desktop arrived" value={fmtBool(snapshot.desktopArrived)} sub={snapshot.desktopArrivedAtUtc ?? undefined} />
|
||||||
<SnapshotItem label="Hello policy" value={fmtHelloPolicy(snapshot)} />
|
<SnapshotItem label="Hello policy" value={fmtHelloPolicy(snapshot)} />
|
||||||
|
{snapshot.helloResolved !== undefined && (
|
||||||
|
<SnapshotItem label="Hello completed" value={fmtBool(snapshot.helloResolved)} />
|
||||||
|
)}
|
||||||
|
{snapshot.realmJoinDetected !== undefined && (
|
||||||
|
<SnapshotItem label="RealmJoin" value={fmtRealmJoin(snapshot)} />
|
||||||
|
)}
|
||||||
<SnapshotItem label="AAD join state" value={snapshot.aadJoinState ?? "unknown"} sub={snapshot.aadJoinStateAtUtc ?? undefined} />
|
<SnapshotItem label="AAD join state" value={snapshot.aadJoinState ?? "unknown"} sub={snapshot.aadJoinStateAtUtc ?? undefined} />
|
||||||
<SnapshotItem label="Reboot observed" value={fmtBool(snapshot.rebootObserved)} />
|
<SnapshotItem label="Reboot observed" value={fmtBool(snapshot.rebootObserved)} />
|
||||||
<SnapshotItem label="Hybrid AAD join" value={snapshot.isHybridJoin === null || snapshot.isHybridJoin === undefined ? "unknown" : fmtBool(snapshot.isHybridJoin)} />
|
<SnapshotItem label="Hybrid AAD join" value={snapshot.isHybridJoin === null || snapshot.isHybridJoin === undefined ? "unknown" : fmtBool(snapshot.isHybridJoin)} />
|
||||||
@@ -135,3 +145,11 @@ function fmtHelloPolicy(s: FailureSnapshot): string {
|
|||||||
if (s.helloPolicyEnabled === false) return "detected (disabled)";
|
if (s.helloPolicyEnabled === false) return "detected (disabled)";
|
||||||
return "detected";
|
return "detected";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function fmtRealmJoin(s: FailureSnapshot): string {
|
||||||
|
if (!s.realmJoinDetected) return "not detected";
|
||||||
|
// "unresolved" = RealmJoin never reported CompletedFirstDeployment (phase 110) and the
|
||||||
|
// 60-min gate timeout wasn't observed either — the deployment was still in flight when
|
||||||
|
// the agent went silent.
|
||||||
|
return s.realmJoinResolved ? "detected (resolved)" : "detected (unresolved)";
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user