mirror of
https://github.com/okieselbach/AutopilotMonitor.git
synced 2026-10-24 04:32:32 +00:00
refactor(decisioncore): WhiteGlove Part 2 + Part2CompletionClassifier + 2 scenarios (M3.4)
Plan §4 M3.4 + plan §2.3 WhiteGlove Part-2 state machine: post-reboot user
sign-in is now a first-class path with a dedicated classifier, a 24h safety
deadline, and an explicit Sealed -> AwaitingUserSignIn -> CompletedPart2 /
Failed progression.
Classifier (WhiteGlovePart2CompletionClassifier : IClassifier):
- Scoring rules NEW (no Legacy equivalent): each of the four Part-2 user
signals (+25) — UserAadSignInComplete, HelloResolvedPart2,
DesktopArrivedPart2, AccountSetupCompletedPart2. All four together hit
the Confirmed threshold (100); missing one drops to Weak (75).
- WhiteGlovePart2CompletionSnapshot implements the new IClassifierSnapshot
interface; existing WhiteGloveSealingSnapshot was retrofit to the same
contract so the harness anti-loop is uniform.
Handlers (DecisionEngine.WhiteGlovePart2.cs):
- SessionRecovered (Shared.cs) now bridges a WhiteGloveSealed state into
WhiteGloveAwaitingUserSignIn and arms the 24h WhiteGlovePart2Safety
deadline on the recovered signal's occurredAt.
- Four Part-2 signal handlers (UserAadSignInComplete, HelloResolvedPart2,
DesktopArrivedPart2, AccountSetupCompletedPart2) record facts + emit a
Part-2 RunClassifier effect; facts arriving outside the AwaitingUserSignIn
stage are recorded but no classifier runs (prevents stray Part-2 verdicts
on non-sealed sessions).
- WhiteGlovePart2Safety deadline handler terminates with Failed /
enrollment_failed reason=part2_user_absent.
- ClassifierVerdictIssued router extended to dispatch on classifier id —
Part-2 verdicts route to ApplyWhiteGlovePart2Verdict; Confirmed triggers
Stage=WhiteGloveCompletedPart2 + Outcome=WhiteGlovePart2Complete +
whiteglove_part2_complete event.
DecisionState: four new SignalFact<DateTime>? fields for the Part-2 signals
(full builder parity).
ClassifierAwareReplayHarness: anti-loop lookup now routes per classifier id
(WhiteGloveSealing <-> WhiteGloveSealing.LastClassifierVerdictId;
Part2Completion <-> WhiteGlovePart2Completion.LastClassifierVerdictId).
Snapshot hashing works through the IClassifierSnapshot interface instead
of a WhiteGloveSealingSnapshot type check.
Scenario fixtures (committed-anonymized):
- whiteglove-part2-happy-v1.jsonl seal Part 1, reboot, 4 Part-2 signals
within minutes -> Confirmed + Part2Complete.
- whiteglove-part2-stuck-v1.jsonl seal Part 1, reboot, 24h passes, safety
deadline fires -> Failed / part2_user_absent.
Tests (3 new in WhiteGlovePart2ScenarioTests):
- Happy terminal state + hypotheses + all four facts + no residual deadlines
- Stuck terminal state + Part-1 Confirmed remains, Part-2 Rejected with reason
- Bridge check: after SessionRecovered(Sealed), Part-2 safety is active
Suite totals: 52 DecisionCore (was 49) / 507 Legacy / 557 Functions.
Plan: plans/REFACTOR_AGENT_V2.md §4 M3 sub-milestone M3.4 + §2.3 Part-2 stages.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1 +1 @@
|
||||
10
|
||||
12
|
||||
|
||||
@@ -1 +1 @@
|
||||
1052
|
||||
1054
|
||||
|
||||
+18
-13
@@ -75,20 +75,15 @@ namespace AutopilotMonitor.DecisionCore.Tests.Harness
|
||||
?? throw new InvalidOperationException(
|
||||
$"RunClassifier effect for '{effect.ClassifierId}' missing snapshot.");
|
||||
|
||||
string snapshotHash;
|
||||
if (snapshot is WhiteGloveSealingSnapshot wgSnap)
|
||||
var hashable = snapshot as IClassifierSnapshot;
|
||||
var snapshotHash = hashable?.ComputeInputHash() ?? string.Empty;
|
||||
var lastVerdictId = LookupLastVerdictId(state, effect.ClassifierId!);
|
||||
if (!string.IsNullOrEmpty(snapshotHash) &&
|
||||
!string.IsNullOrEmpty(lastVerdictId) &&
|
||||
snapshotHash == lastVerdictId)
|
||||
{
|
||||
snapshotHash = wgSnap.ComputeInputHash();
|
||||
if (state.WhiteGloveSealing.LastClassifierVerdictId == snapshotHash)
|
||||
{
|
||||
Increment(effect.ClassifierId!, "skipped_by_antiloop");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// No per-classifier state tracking yet; run unconditionally.
|
||||
snapshotHash = string.Empty;
|
||||
Increment(effect.ClassifierId!, "skipped_by_antiloop");
|
||||
continue;
|
||||
}
|
||||
|
||||
var verdict = classifier.Classify(snapshot);
|
||||
@@ -130,6 +125,16 @@ namespace AutopilotMonitor.DecisionCore.Tests.Harness
|
||||
/// <summary>Run counts per classifier (keys: "run", "skipped_by_antiloop"). For test assertions.</summary>
|
||||
public IDictionary<string, int> ClassifierRunStats { get; private set; } = new Dictionary<string, int>();
|
||||
|
||||
private static string? LookupLastVerdictId(DecisionState state, string classifierId)
|
||||
{
|
||||
return classifierId switch
|
||||
{
|
||||
WhiteGloveSealingClassifier.ClassifierId => state.WhiteGloveSealing.LastClassifierVerdictId,
|
||||
WhiteGlovePart2CompletionClassifier.ClassifierId => state.WhiteGlovePart2Completion.LastClassifierVerdictId,
|
||||
_ => null,
|
||||
};
|
||||
}
|
||||
|
||||
private void Increment(string classifierId, string bucket)
|
||||
{
|
||||
var key = $"{classifierId}:{bucket}";
|
||||
|
||||
+91
@@ -0,0 +1,91 @@
|
||||
using System.Collections.Generic;
|
||||
using AutopilotMonitor.DecisionCore.Classifiers;
|
||||
using AutopilotMonitor.DecisionCore.Engine;
|
||||
using AutopilotMonitor.DecisionCore.State;
|
||||
using AutopilotMonitor.DecisionCore.Tests.Harness;
|
||||
using Xunit;
|
||||
|
||||
namespace AutopilotMonitor.DecisionCore.Tests.Scenarios
|
||||
{
|
||||
/// <summary>
|
||||
/// Plan §4 M3.4 — WhiteGlove Part 2 scenarios.
|
||||
/// </summary>
|
||||
public sealed class WhiteGlovePart2ScenarioTests : ScenarioTestBase
|
||||
{
|
||||
private static ClassifierAwareReplayHarness NewHarness() =>
|
||||
new ClassifierAwareReplayHarness(
|
||||
new DecisionEngine(),
|
||||
new Dictionary<string, IClassifier>
|
||||
{
|
||||
[WhiteGloveSealingClassifier.ClassifierId] = new WhiteGloveSealingClassifier(),
|
||||
[WhiteGlovePart2CompletionClassifier.ClassifierId] = new WhiteGlovePart2CompletionClassifier(),
|
||||
});
|
||||
|
||||
[Fact]
|
||||
public void WhiteGlovePart2Happy_fourUserSignals_sealPart2()
|
||||
{
|
||||
var signals = LoadFixture("whiteglove-part2-happy-v1.jsonl");
|
||||
var result = NewHarness().Replay("session-anon-0020", "tenant-anon-0020", signals);
|
||||
|
||||
Assert.Equal(SessionStage.WhiteGloveCompletedPart2, result.FinalState.Stage);
|
||||
Assert.Equal(SessionOutcome.WhiteGlovePart2Complete, result.FinalState.Outcome);
|
||||
Assert.Equal(HypothesisLevel.Confirmed, result.FinalState.WhiteGlovePart2Completion.Level);
|
||||
Assert.Equal(100, result.FinalState.WhiteGlovePart2Completion.Score);
|
||||
|
||||
// All four Part-2 facts should be present.
|
||||
Assert.NotNull(result.FinalState.UserAadSignInCompleteUtc);
|
||||
Assert.NotNull(result.FinalState.HelloResolvedPart2Utc);
|
||||
Assert.NotNull(result.FinalState.DesktopArrivedPart2Utc);
|
||||
Assert.NotNull(result.FinalState.AccountSetupCompletedPart2Utc);
|
||||
|
||||
// Part-1 hypothesis still Confirmed (not reset by the Part-2 transition).
|
||||
Assert.Equal(HypothesisLevel.Confirmed, result.FinalState.WhiteGloveSealing.Level);
|
||||
|
||||
// SystemReboot fact recorded from SessionRecovered.
|
||||
Assert.NotNull(result.FinalState.SystemRebootUtc);
|
||||
|
||||
Assert.Empty(result.FinalState.Deadlines);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WhiteGlovePart2Stuck_safetyDeadlineFires_failsSession()
|
||||
{
|
||||
var signals = LoadFixture("whiteglove-part2-stuck-v1.jsonl");
|
||||
var result = NewHarness().Replay("session-anon-0021", "tenant-anon-0021", signals);
|
||||
|
||||
Assert.Equal(SessionStage.Failed, result.FinalState.Stage);
|
||||
Assert.Equal(SessionOutcome.EnrollmentFailed, result.FinalState.Outcome);
|
||||
Assert.Equal(HypothesisLevel.Rejected, result.FinalState.WhiteGlovePart2Completion.Level);
|
||||
Assert.Equal("part2_user_absent", result.FinalState.WhiteGlovePart2Completion.Reason);
|
||||
|
||||
// No user-signal facts captured.
|
||||
Assert.Null(result.FinalState.UserAadSignInCompleteUtc);
|
||||
Assert.Null(result.FinalState.HelloResolvedPart2Utc);
|
||||
Assert.Null(result.FinalState.DesktopArrivedPart2Utc);
|
||||
Assert.Null(result.FinalState.AccountSetupCompletedPart2Utc);
|
||||
|
||||
// Part 1 verdict remains Confirmed even as Part 2 failed.
|
||||
Assert.Equal(HypothesisLevel.Confirmed, result.FinalState.WhiteGloveSealing.Level);
|
||||
Assert.Empty(result.FinalState.Deadlines);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WhiteGlovePart2_bridge_armsSafetyDeadline_onRecoverFromSealed()
|
||||
{
|
||||
// Intermediate-state check by running a shortened fixture: after Part-1 seals and
|
||||
// SessionRecovered fires, the safety deadline should be active. The happy path
|
||||
// then cancels it when the classifier reaches Confirmed; here we just verify the
|
||||
// bridge behaviour by loading only the first 4 signals of the happy fixture.
|
||||
var full = LoadFixture("whiteglove-part2-happy-v1.jsonl");
|
||||
var prefix = new List<AutopilotMonitor.DecisionCore.Signals.DecisionSignal>();
|
||||
for (int i = 0; i < 4 && i < full.Count; i++) prefix.Add(full[i]);
|
||||
|
||||
var result = NewHarness().Replay("session-anon-0022", "tenant-anon-0022", prefix);
|
||||
|
||||
Assert.Equal(SessionStage.WhiteGloveAwaitingUserSignIn, result.FinalState.Stage);
|
||||
Assert.Contains(
|
||||
result.FinalState.Deadlines,
|
||||
d => d.Name == DeadlineNames.WhiteGlovePart2Safety);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
namespace AutopilotMonitor.DecisionCore.Classifiers
|
||||
{
|
||||
/// <summary>
|
||||
/// A classifier-input snapshot that exposes a deterministic hash. Plan §2.4 anti-loop.
|
||||
/// <para>
|
||||
/// Implementations must produce the same hash byte-for-byte for equivalent snapshots,
|
||||
/// across process restarts, regardless of hashtable ordering or culture. The hash is
|
||||
/// used by the effect runner / replay harness to skip classifier invocations whose
|
||||
/// input matches the last verdict's <see cref="ClassifierVerdict.InputHash"/>.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public interface IClassifierSnapshot
|
||||
{
|
||||
string ComputeInputHash();
|
||||
}
|
||||
}
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using AutopilotMonitor.DecisionCore.State;
|
||||
|
||||
namespace AutopilotMonitor.DecisionCore.Classifiers
|
||||
{
|
||||
/// <summary>
|
||||
/// Scoring classifier for WhiteGlove Part 2 completion. Plan §2.4 / §M3.4.
|
||||
/// <para>
|
||||
/// Scoring rules (new — no Legacy equivalent): each of the four Part-2 post-reboot user
|
||||
/// signals contributes +25; all four together cross the Confirmed threshold of 100.
|
||||
/// Missing one signal gives 75 -> Weak. This matches the plan's requirement that Part-2
|
||||
/// completion needs the full UserSignIn + Hello + Desktop + AccountSetup set.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public sealed class WhiteGlovePart2CompletionClassifier : IClassifier
|
||||
{
|
||||
public const string ClassifierId = "whiteglove-part2-completion";
|
||||
|
||||
internal const int HighThreshold = 100; // exactly-all-four -> Confirmed
|
||||
internal const int LowThreshold = 25; // any single signal -> Weak
|
||||
|
||||
internal const int WeightUserAadSignIn = 25;
|
||||
internal const int WeightHelloResolved = 25;
|
||||
internal const int WeightDesktopArrived = 25;
|
||||
internal const int WeightAccountSetup = 25;
|
||||
|
||||
public string Id => ClassifierId;
|
||||
public Type SnapshotType => typeof(WhiteGlovePart2CompletionSnapshot);
|
||||
|
||||
public ClassifierVerdict Classify(object snapshot)
|
||||
{
|
||||
if (snapshot == null) throw new ArgumentNullException(nameof(snapshot));
|
||||
if (!(snapshot is WhiteGlovePart2CompletionSnapshot s))
|
||||
{
|
||||
throw new ArgumentException(
|
||||
$"Expected {nameof(WhiteGlovePart2CompletionSnapshot)}, got {snapshot.GetType().Name}.",
|
||||
nameof(snapshot));
|
||||
}
|
||||
|
||||
var factors = new List<string>();
|
||||
int score = 0;
|
||||
|
||||
if (s.UserAadSignInComplete) { score += WeightUserAadSignIn; factors.Add($"user_aad_signin:{WeightUserAadSignIn:+#;-#;0}"); }
|
||||
if (s.HelloResolvedPart2) { score += WeightHelloResolved; factors.Add($"hello_part2:{WeightHelloResolved:+#;-#;0}"); }
|
||||
if (s.DesktopArrivedPart2) { score += WeightDesktopArrived; factors.Add($"desktop_part2:{WeightDesktopArrived:+#;-#;0}"); }
|
||||
if (s.AccountSetupCompletedPart2) { score += WeightAccountSetup; factors.Add($"account_setup_part2:{WeightAccountSetup:+#;-#;0}"); }
|
||||
|
||||
if (score < 0) score = 0;
|
||||
if (score > 100) score = 100;
|
||||
|
||||
HypothesisLevel level;
|
||||
if (score >= HighThreshold) level = HypothesisLevel.Confirmed;
|
||||
else if (score >= LowThreshold) level = HypothesisLevel.Weak;
|
||||
else level = HypothesisLevel.Unknown;
|
||||
|
||||
return new ClassifierVerdict(
|
||||
classifierId: ClassifierId,
|
||||
level: level,
|
||||
score: score,
|
||||
contributingFactors: factors,
|
||||
reason: $"confidence={level} score={score}",
|
||||
inputHash: s.ComputeInputHash());
|
||||
}
|
||||
}
|
||||
}
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
using System;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using AutopilotMonitor.Shared.Models;
|
||||
|
||||
namespace AutopilotMonitor.DecisionCore.Classifiers
|
||||
{
|
||||
/// <summary>
|
||||
/// Input snapshot for <see cref="WhiteGlovePart2CompletionClassifier"/>.
|
||||
/// Plan §2.4 / §M3.4 — the four Part-2 post-reboot user signals drive the score.
|
||||
/// </summary>
|
||||
public sealed class WhiteGlovePart2CompletionSnapshot : IClassifierSnapshot
|
||||
{
|
||||
public WhiteGlovePart2CompletionSnapshot(
|
||||
bool userAadSignInComplete,
|
||||
bool helloResolvedPart2,
|
||||
bool desktopArrivedPart2,
|
||||
bool accountSetupCompletedPart2,
|
||||
EnrollmentPhase? currentEnrollmentPhase,
|
||||
DateTime? systemRebootUtc)
|
||||
{
|
||||
UserAadSignInComplete = userAadSignInComplete;
|
||||
HelloResolvedPart2 = helloResolvedPart2;
|
||||
DesktopArrivedPart2 = desktopArrivedPart2;
|
||||
AccountSetupCompletedPart2 = accountSetupCompletedPart2;
|
||||
CurrentEnrollmentPhase = currentEnrollmentPhase;
|
||||
SystemRebootUtc = systemRebootUtc;
|
||||
}
|
||||
|
||||
public bool UserAadSignInComplete { get; }
|
||||
public bool HelloResolvedPart2 { get; }
|
||||
public bool DesktopArrivedPart2 { get; }
|
||||
public bool AccountSetupCompletedPart2 { get; }
|
||||
public EnrollmentPhase? CurrentEnrollmentPhase { get; }
|
||||
public DateTime? SystemRebootUtc { get; }
|
||||
|
||||
public string ComputeInputHash()
|
||||
{
|
||||
var canonical = string.Join(
|
||||
"|",
|
||||
"whiteglove-part2-completion-v1",
|
||||
UserAadSignInComplete ? "1" : "0",
|
||||
HelloResolvedPart2 ? "1" : "0",
|
||||
DesktopArrivedPart2 ? "1" : "0",
|
||||
AccountSetupCompletedPart2 ? "1" : "0",
|
||||
CurrentEnrollmentPhase?.ToString() ?? "null",
|
||||
SystemRebootUtc?.ToString("O") ?? "null");
|
||||
|
||||
using (var sha = SHA256.Create())
|
||||
{
|
||||
var bytes = sha.ComputeHash(Encoding.UTF8.GetBytes(canonical));
|
||||
var sb = new StringBuilder(16);
|
||||
for (int i = 0; i < 8; i++) sb.Append(bytes[i].ToString("x2"));
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,7 @@ namespace AutopilotMonitor.DecisionCore.Classifiers
|
||||
/// would cause unnecessary re-runs on the anti-loop guard.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public sealed class WhiteGloveSealingSnapshot
|
||||
public sealed class WhiteGloveSealingSnapshot : IClassifierSnapshot
|
||||
{
|
||||
public WhiteGloveSealingSnapshot(
|
||||
bool shellCoreWhiteGloveSuccessSeen,
|
||||
|
||||
@@ -68,8 +68,16 @@ namespace AutopilotMonitor.DecisionCore.Engine
|
||||
/// </summary>
|
||||
private DecisionStep HandleSessionRecoveredV1(DecisionState state, DecisionSignal signal)
|
||||
{
|
||||
// M3.4 will add WhiteGlove-Part-2 transition here. For M3.0 the handler is a
|
||||
// neutral "observed a restart" step — stage unchanged, StepIndex advanced.
|
||||
// Plan §2.7 sonder-case 1: WhiteGlove Part 1 -> Reboot -> Part 2.
|
||||
// If the recovered session was sealed, transition into the Part 2 awaiting-user
|
||||
// stage and arm the 24h safety deadline. See DecisionEngine.WhiteGlovePart2.cs.
|
||||
if (state.Stage == SessionStage.WhiteGloveSealed)
|
||||
{
|
||||
return HandleWhiteGlovePart1To2Bridge(state, signal);
|
||||
}
|
||||
|
||||
// Otherwise the recovered state is already mid-flight elsewhere; the handler is
|
||||
// a neutral "observed a restart" step — stage unchanged, bookkeeping advanced.
|
||||
var newState = BumpStepBookkeeping(state, signal);
|
||||
var transition = BuildTakenTransition(
|
||||
before: state,
|
||||
@@ -156,6 +164,8 @@ namespace AutopilotMonitor.DecisionCore.Engine
|
||||
return HandleDeviceOnlyEspDetectionDeadlineFired(state, signal);
|
||||
case DeadlineNames.ClassifierTick:
|
||||
return HandleClassifierTickDeadlineFired(state, signal);
|
||||
case DeadlineNames.WhiteGlovePart2Safety:
|
||||
return HandleWhiteGlovePart2SafetyDeadlineFired(state, signal);
|
||||
default:
|
||||
// Deadline name not recognized in this sub-milestone. Cancel it from state
|
||||
// and record a neutral taken transition — M3.3+ adds handlers for
|
||||
|
||||
@@ -97,7 +97,12 @@ namespace AutopilotMonitor.DecisionCore.Engine
|
||||
.WithStepIndex(nextStep)
|
||||
.WithLastAppliedSignalOrdinal(signal.SessionSignalOrdinal);
|
||||
|
||||
// Only the sealing classifier is wired in M3.3 — M3.4 adds Part 2.
|
||||
// M3.4 Part-2 classifier runs through its own applier.
|
||||
if (classifier == WhiteGlovePart2CompletionClassifier.ClassifierId)
|
||||
{
|
||||
return ApplyWhiteGlovePart2Verdict(state, signal, level, score, reason, inputHash);
|
||||
}
|
||||
|
||||
if (classifier == WhiteGloveSealingClassifier.ClassifierId)
|
||||
{
|
||||
builder.WhiteGloveSealing = state.WhiteGloveSealing.With(
|
||||
|
||||
@@ -0,0 +1,257 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using AutopilotMonitor.DecisionCore.Classifiers;
|
||||
using AutopilotMonitor.DecisionCore.Signals;
|
||||
using AutopilotMonitor.DecisionCore.State;
|
||||
|
||||
namespace AutopilotMonitor.DecisionCore.Engine
|
||||
{
|
||||
// WhiteGlove Part 2 (post-reboot user sign-in) handlers. Plan §2.5 / §M3.4.
|
||||
public sealed partial class DecisionEngine
|
||||
{
|
||||
// Plan §2.3: "Part-2-Safety-Timeout (z.B. 24h nach Reboot)" watches for the user
|
||||
// never signing in. Firing yields Outcome=EnrollmentFailed reason=part2_user_absent.
|
||||
internal static readonly TimeSpan s_whiteGlovePart2SafetyWindow = TimeSpan.FromHours(24);
|
||||
|
||||
/// <summary>
|
||||
/// WhiteGlove Part 1 → Part 2 bridge. Called from <see cref="HandleSessionRecoveredV1"/>
|
||||
/// when the recovered state is <see cref="SessionStage.WhiteGloveSealed"/>. Transitions
|
||||
/// into <see cref="SessionStage.WhiteGloveAwaitingUserSignIn"/> and arms the Part-2
|
||||
/// safety deadline.
|
||||
/// </summary>
|
||||
private DecisionStep HandleWhiteGlovePart1To2Bridge(DecisionState state, DecisionSignal signal)
|
||||
{
|
||||
var nextStep = state.StepIndex + 1;
|
||||
var safety = new ActiveDeadline(
|
||||
name: DeadlineNames.WhiteGlovePart2Safety,
|
||||
dueAtUtc: signal.OccurredAtUtc.Add(s_whiteGlovePart2SafetyWindow),
|
||||
firesSignalKind: DecisionSignalKind.DeadlineFired,
|
||||
firesPayload: new Dictionary<string, string>
|
||||
{
|
||||
[SignalPayloadKeys.Deadline] = DeadlineNames.WhiteGlovePart2Safety,
|
||||
});
|
||||
|
||||
var builder = state.ToBuilder()
|
||||
.WithStage(SessionStage.WhiteGloveAwaitingUserSignIn)
|
||||
.WithOutcome(null) // leaving the Part-1 Sealed outcome aside until Part-2 concludes.
|
||||
.WithStepIndex(nextStep)
|
||||
.WithLastAppliedSignalOrdinal(signal.SessionSignalOrdinal)
|
||||
.AddDeadline(safety);
|
||||
builder.SystemRebootUtc = new SignalFact<DateTime>(signal.OccurredAtUtc, signal.SessionSignalOrdinal);
|
||||
|
||||
var newState = builder.Build();
|
||||
|
||||
var transition = BuildTakenTransition(
|
||||
before: state,
|
||||
signal: signal,
|
||||
toStage: SessionStage.WhiteGloveAwaitingUserSignIn,
|
||||
nextStepIndex: nextStep,
|
||||
trigger: "SessionRecovered:WhiteGloveSealed->AwaitingUserSignIn");
|
||||
|
||||
var effects = new[]
|
||||
{
|
||||
new DecisionEffect(DecisionEffectKind.ScheduleDeadline, deadline: safety),
|
||||
};
|
||||
|
||||
return new DecisionStep(newState, transition, effects);
|
||||
}
|
||||
|
||||
private DecisionStep HandleUserAadSignInCompleteV1(DecisionState state, DecisionSignal signal)
|
||||
{
|
||||
return UpdatePart2Fact(
|
||||
state,
|
||||
signal,
|
||||
applyFact: b => { b.UserAadSignInCompleteUtc = new SignalFact<DateTime>(signal.OccurredAtUtc, signal.SessionSignalOrdinal); },
|
||||
trigger: nameof(DecisionSignalKind.UserAadSignInComplete));
|
||||
}
|
||||
|
||||
private DecisionStep HandleHelloResolvedPart2V1(DecisionState state, DecisionSignal signal)
|
||||
{
|
||||
return UpdatePart2Fact(
|
||||
state,
|
||||
signal,
|
||||
applyFact: b => { b.HelloResolvedPart2Utc = new SignalFact<DateTime>(signal.OccurredAtUtc, signal.SessionSignalOrdinal); },
|
||||
trigger: nameof(DecisionSignalKind.HelloResolvedPart2));
|
||||
}
|
||||
|
||||
private DecisionStep HandleDesktopArrivedPart2V1(DecisionState state, DecisionSignal signal)
|
||||
{
|
||||
return UpdatePart2Fact(
|
||||
state,
|
||||
signal,
|
||||
applyFact: b => { b.DesktopArrivedPart2Utc = new SignalFact<DateTime>(signal.OccurredAtUtc, signal.SessionSignalOrdinal); },
|
||||
trigger: nameof(DecisionSignalKind.DesktopArrivedPart2));
|
||||
}
|
||||
|
||||
private DecisionStep HandleAccountSetupCompletedPart2V1(DecisionState state, DecisionSignal signal)
|
||||
{
|
||||
return UpdatePart2Fact(
|
||||
state,
|
||||
signal,
|
||||
applyFact: b => { b.AccountSetupCompletedPart2Utc = new SignalFact<DateTime>(signal.OccurredAtUtc, signal.SessionSignalOrdinal); },
|
||||
trigger: nameof(DecisionSignalKind.AccountSetupCompletedPart2));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Part-2 safety deadline fired — user never signed in within 24 h. Terminates the
|
||||
/// session as <see cref="SessionOutcome.EnrollmentFailed"/> with reason
|
||||
/// <c>part2_user_absent</c>.
|
||||
/// </summary>
|
||||
private DecisionStep HandleWhiteGlovePart2SafetyDeadlineFired(DecisionState state, DecisionSignal signal)
|
||||
{
|
||||
var nextStep = state.StepIndex + 1;
|
||||
|
||||
var builder = state.ToBuilder()
|
||||
.WithStage(SessionStage.Failed)
|
||||
.WithOutcome(SessionOutcome.EnrollmentFailed)
|
||||
.WithStepIndex(nextStep)
|
||||
.WithLastAppliedSignalOrdinal(signal.SessionSignalOrdinal)
|
||||
.ClearDeadlines();
|
||||
|
||||
builder.WhiteGlovePart2Completion = state.WhiteGlovePart2Completion.With(
|
||||
level: HypothesisLevel.Rejected,
|
||||
reason: "part2_user_absent",
|
||||
score: 0,
|
||||
lastUpdatedUtc: signal.OccurredAtUtc);
|
||||
|
||||
var newState = builder.Build();
|
||||
|
||||
var transition = BuildTakenTransition(
|
||||
before: state,
|
||||
signal: signal,
|
||||
toStage: SessionStage.Failed,
|
||||
nextStepIndex: nextStep,
|
||||
trigger: $"DeadlineFired:{DeadlineNames.WhiteGlovePart2Safety}");
|
||||
|
||||
var effects = new[]
|
||||
{
|
||||
new DecisionEffect(
|
||||
DecisionEffectKind.EmitEventTimelineEntry,
|
||||
parameters: new Dictionary<string, string>
|
||||
{
|
||||
["eventType"] = "enrollment_failed",
|
||||
["reason"] = "part2_user_absent",
|
||||
}),
|
||||
};
|
||||
|
||||
return new DecisionStep(newState, transition, effects);
|
||||
}
|
||||
|
||||
// ============================================================ internal helpers
|
||||
|
||||
private DecisionStep UpdatePart2Fact(
|
||||
DecisionState state,
|
||||
DecisionSignal signal,
|
||||
Action<DecisionStateBuilder> applyFact,
|
||||
string trigger)
|
||||
{
|
||||
var nextStep = state.StepIndex + 1;
|
||||
var builder = state.ToBuilder()
|
||||
.WithStepIndex(nextStep)
|
||||
.WithLastAppliedSignalOrdinal(signal.SessionSignalOrdinal);
|
||||
|
||||
applyFact(builder);
|
||||
|
||||
// Part-2 only matters once we're in the WhiteGloveAwaitingUserSignIn stage;
|
||||
// if signals arrive on other stages, still record the fact but do not emit
|
||||
// a Part-2 classifier run.
|
||||
var emitClassifier =
|
||||
state.Stage == SessionStage.WhiteGloveAwaitingUserSignIn
|
||||
|| state.Stage == SessionStage.WhiteGloveCompletedPart2;
|
||||
|
||||
var stateAfterFact = builder.Build();
|
||||
|
||||
var effects = emitClassifier
|
||||
? new[] { BuildRunPart2ClassifierEffect(stateAfterFact) }
|
||||
: Array.Empty<DecisionEffect>();
|
||||
|
||||
var transition = BuildTakenTransition(
|
||||
before: state,
|
||||
signal: signal,
|
||||
toStage: stateAfterFact.Stage,
|
||||
nextStepIndex: nextStep,
|
||||
trigger: trigger);
|
||||
|
||||
return new DecisionStep(stateAfterFact, transition, effects);
|
||||
}
|
||||
|
||||
private static DecisionEffect BuildRunPart2ClassifierEffect(DecisionState state) =>
|
||||
new DecisionEffect(
|
||||
kind: DecisionEffectKind.RunClassifier,
|
||||
classifierId: WhiteGlovePart2CompletionClassifier.ClassifierId,
|
||||
classifierSnapshot: BuildWhiteGlovePart2CompletionSnapshot(state));
|
||||
|
||||
internal static WhiteGlovePart2CompletionSnapshot BuildWhiteGlovePart2CompletionSnapshot(DecisionState state) =>
|
||||
new WhiteGlovePart2CompletionSnapshot(
|
||||
userAadSignInComplete: state.UserAadSignInCompleteUtc != null,
|
||||
helloResolvedPart2: state.HelloResolvedPart2Utc != null,
|
||||
desktopArrivedPart2: state.DesktopArrivedPart2Utc != null,
|
||||
accountSetupCompletedPart2: state.AccountSetupCompletedPart2Utc != null,
|
||||
currentEnrollmentPhase: state.CurrentEnrollmentPhase?.Value,
|
||||
systemRebootUtc: state.SystemRebootUtc?.Value);
|
||||
|
||||
/// <summary>
|
||||
/// Apply a Part-2 classifier verdict to <see cref="DecisionState.WhiteGlovePart2Completion"/>.
|
||||
/// Called from the extended <see cref="HandleClassifierVerdictIssuedV1"/> router when
|
||||
/// the verdict's classifier id is the Part-2 completion classifier. On Confirmed,
|
||||
/// transitions to <see cref="SessionStage.WhiteGloveCompletedPart2"/> + Outcome
|
||||
/// <see cref="SessionOutcome.WhiteGlovePart2Complete"/> and emits the
|
||||
/// <c>whiteglove_part2_complete</c> event.
|
||||
/// </summary>
|
||||
private DecisionStep ApplyWhiteGlovePart2Verdict(
|
||||
DecisionState state,
|
||||
DecisionSignal signal,
|
||||
HypothesisLevel level,
|
||||
int score,
|
||||
string reason,
|
||||
string inputHash)
|
||||
{
|
||||
var nextStep = state.StepIndex + 1;
|
||||
var builder = state.ToBuilder()
|
||||
.WithStepIndex(nextStep)
|
||||
.WithLastAppliedSignalOrdinal(signal.SessionSignalOrdinal);
|
||||
|
||||
builder.WhiteGlovePart2Completion = state.WhiteGlovePart2Completion.With(
|
||||
level: level,
|
||||
reason: reason,
|
||||
score: score,
|
||||
lastUpdatedUtc: signal.OccurredAtUtc,
|
||||
lastClassifierVerdictId: inputHash);
|
||||
|
||||
if (level == HypothesisLevel.Confirmed)
|
||||
{
|
||||
builder
|
||||
.WithStage(SessionStage.WhiteGloveCompletedPart2)
|
||||
.WithOutcome(SessionOutcome.WhiteGlovePart2Complete)
|
||||
.ClearDeadlines();
|
||||
|
||||
var sealedState = builder.Build();
|
||||
var sealedTransition = BuildTakenTransition(
|
||||
before: state,
|
||||
signal: signal,
|
||||
toStage: SessionStage.WhiteGloveCompletedPart2,
|
||||
nextStepIndex: nextStep,
|
||||
trigger: $"ClassifierVerdictIssued:{WhiteGlovePart2CompletionClassifier.ClassifierId}:Confirmed");
|
||||
|
||||
var effects = new[]
|
||||
{
|
||||
new DecisionEffect(
|
||||
DecisionEffectKind.EmitEventTimelineEntry,
|
||||
parameters: new Dictionary<string, string> { ["eventType"] = "whiteglove_part2_complete" }),
|
||||
};
|
||||
|
||||
return new DecisionStep(sealedState, sealedTransition, effects);
|
||||
}
|
||||
|
||||
var newState = builder.Build();
|
||||
var transition = BuildTakenTransition(
|
||||
before: state,
|
||||
signal: signal,
|
||||
toStage: state.Stage,
|
||||
nextStepIndex: nextStep,
|
||||
trigger: $"ClassifierVerdictIssued:{WhiteGlovePart2CompletionClassifier.ClassifierId}:{level}");
|
||||
|
||||
return new DecisionStep(newState, transition, Array.Empty<DecisionEffect>());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -77,7 +77,13 @@ namespace AutopilotMonitor.DecisionCore.Engine
|
||||
(DecisionSignalKind.WhiteGloveSealingPatternDetected, 1) => HandleWhiteGloveSealingPatternDetectedV1(state, signal),
|
||||
(DecisionSignalKind.ClassifierVerdictIssued, 1) => HandleClassifierVerdictIssuedV1(state, signal),
|
||||
|
||||
// ----- Not yet wired (WhiteGlove Part 2 in M3.4, edge in M3.5) -----
|
||||
// ----- WhiteGlove Part 2 (DecisionEngine.WhiteGlovePart2.cs) -----
|
||||
(DecisionSignalKind.UserAadSignInComplete, 1) => HandleUserAadSignInCompleteV1(state, signal),
|
||||
(DecisionSignalKind.HelloResolvedPart2, 1) => HandleHelloResolvedPart2V1(state, signal),
|
||||
(DecisionSignalKind.DesktopArrivedPart2, 1) => HandleDesktopArrivedPart2V1(state, signal),
|
||||
(DecisionSignalKind.AccountSetupCompletedPart2, 1) => HandleAccountSetupCompletedPart2V1(state, signal),
|
||||
|
||||
// ----- Not yet wired (edge in M3.5) -----
|
||||
_ => HandleUnhandledSignal(state, signal),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -42,6 +42,10 @@ namespace AutopilotMonitor.DecisionCore.State
|
||||
SignalFact<string>? imeMatchedPatternId,
|
||||
SignalFact<bool>? shellCoreWhiteGloveSuccessSeen,
|
||||
SignalFact<bool>? whiteGloveSealingPatternSeen,
|
||||
SignalFact<DateTime>? userAadSignInCompleteUtc,
|
||||
SignalFact<DateTime>? helloResolvedPart2Utc,
|
||||
SignalFact<DateTime>? desktopArrivedPart2Utc,
|
||||
SignalFact<DateTime>? accountSetupCompletedPart2Utc,
|
||||
IReadOnlyList<ActiveDeadline> deadlines,
|
||||
long lastAppliedSignalOrdinal,
|
||||
int stepIndex,
|
||||
@@ -78,6 +82,10 @@ namespace AutopilotMonitor.DecisionCore.State
|
||||
ImeMatchedPatternId = imeMatchedPatternId;
|
||||
ShellCoreWhiteGloveSuccessSeen = shellCoreWhiteGloveSuccessSeen;
|
||||
WhiteGloveSealingPatternSeen = whiteGloveSealingPatternSeen;
|
||||
UserAadSignInCompleteUtc = userAadSignInCompleteUtc;
|
||||
HelloResolvedPart2Utc = helloResolvedPart2Utc;
|
||||
DesktopArrivedPart2Utc = desktopArrivedPart2Utc;
|
||||
AccountSetupCompletedPart2Utc = accountSetupCompletedPart2Utc;
|
||||
Deadlines = deadlines ?? throw new ArgumentNullException(nameof(deadlines));
|
||||
LastAppliedSignalOrdinal = lastAppliedSignalOrdinal;
|
||||
StepIndex = stepIndex;
|
||||
@@ -133,6 +141,12 @@ namespace AutopilotMonitor.DecisionCore.State
|
||||
/// <summary>True once <see cref="Signals.DecisionSignalKind.WhiteGloveSealingPatternDetected"/> has fired. Signal-correlated WG path.</summary>
|
||||
public SignalFact<bool>? WhiteGloveSealingPatternSeen { get; }
|
||||
|
||||
// --- WhiteGlove Part 2 (post-reboot user sign-in) facts ---
|
||||
public SignalFact<DateTime>? UserAadSignInCompleteUtc { get; }
|
||||
public SignalFact<DateTime>? HelloResolvedPart2Utc { get; }
|
||||
public SignalFact<DateTime>? DesktopArrivedPart2Utc { get; }
|
||||
public SignalFact<DateTime>? AccountSetupCompletedPart2Utc { get; }
|
||||
|
||||
public IReadOnlyList<ActiveDeadline> Deadlines { get; }
|
||||
|
||||
public long LastAppliedSignalOrdinal { get; }
|
||||
@@ -175,6 +189,10 @@ namespace AutopilotMonitor.DecisionCore.State
|
||||
imeMatchedPatternId: null,
|
||||
shellCoreWhiteGloveSuccessSeen: null,
|
||||
whiteGloveSealingPatternSeen: null,
|
||||
userAadSignInCompleteUtc: null,
|
||||
helloResolvedPart2Utc: null,
|
||||
desktopArrivedPart2Utc: null,
|
||||
accountSetupCompletedPart2Utc: null,
|
||||
deadlines: Array.Empty<ActiveDeadline>(),
|
||||
lastAppliedSignalOrdinal: -1,
|
||||
stepIndex: 0);
|
||||
|
||||
@@ -40,6 +40,10 @@ namespace AutopilotMonitor.DecisionCore.State
|
||||
ImeMatchedPatternId = source.ImeMatchedPatternId;
|
||||
ShellCoreWhiteGloveSuccessSeen = source.ShellCoreWhiteGloveSuccessSeen;
|
||||
WhiteGloveSealingPatternSeen = source.WhiteGloveSealingPatternSeen;
|
||||
UserAadSignInCompleteUtc = source.UserAadSignInCompleteUtc;
|
||||
HelloResolvedPart2Utc = source.HelloResolvedPart2Utc;
|
||||
DesktopArrivedPart2Utc = source.DesktopArrivedPart2Utc;
|
||||
AccountSetupCompletedPart2Utc = source.AccountSetupCompletedPart2Utc;
|
||||
Deadlines = new List<ActiveDeadline>(source.Deadlines);
|
||||
LastAppliedSignalOrdinal = source.LastAppliedSignalOrdinal;
|
||||
StepIndex = source.StepIndex;
|
||||
@@ -67,6 +71,10 @@ namespace AutopilotMonitor.DecisionCore.State
|
||||
public SignalFact<string>? ImeMatchedPatternId { get; set; }
|
||||
public SignalFact<bool>? ShellCoreWhiteGloveSuccessSeen { get; set; }
|
||||
public SignalFact<bool>? WhiteGloveSealingPatternSeen { get; set; }
|
||||
public SignalFact<DateTime>? UserAadSignInCompleteUtc { get; set; }
|
||||
public SignalFact<DateTime>? HelloResolvedPart2Utc { get; set; }
|
||||
public SignalFact<DateTime>? DesktopArrivedPart2Utc { get; set; }
|
||||
public SignalFact<DateTime>? AccountSetupCompletedPart2Utc { get; set; }
|
||||
public List<ActiveDeadline> Deadlines { get; set; }
|
||||
public long LastAppliedSignalOrdinal { get; set; }
|
||||
public int StepIndex { get; set; }
|
||||
@@ -136,6 +144,10 @@ namespace AutopilotMonitor.DecisionCore.State
|
||||
imeMatchedPatternId: ImeMatchedPatternId,
|
||||
shellCoreWhiteGloveSuccessSeen: ShellCoreWhiteGloveSuccessSeen,
|
||||
whiteGloveSealingPatternSeen: WhiteGloveSealingPatternSeen,
|
||||
userAadSignInCompleteUtc: UserAadSignInCompleteUtc,
|
||||
helloResolvedPart2Utc: HelloResolvedPart2Utc,
|
||||
desktopArrivedPart2Utc: DesktopArrivedPart2Utc,
|
||||
accountSetupCompletedPart2Utc: AccountSetupCompletedPart2Utc,
|
||||
deadlines: Deadlines.ToArray(),
|
||||
lastAppliedSignalOrdinal: LastAppliedSignalOrdinal,
|
||||
stepIndex: StepIndex,
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
# Scenario: WhiteGlove Part 2 Happy (Post-Reboot user sign-in)
|
||||
# Session picks up from a WhiteGloveSealed Part 1 state (simulated here by driving
|
||||
# ShellCoreWhiteGloveSuccess first, which confirms Part 1 inline). SessionRecovered
|
||||
# after a reboot transitions the session to WhiteGloveAwaitingUserSignIn and arms the
|
||||
# 24h Part-2 safety deadline. All four Part-2 user signals arrive within minutes ->
|
||||
# Part2 completion classifier returns Confirmed -> Stage=WhiteGloveCompletedPart2 +
|
||||
# Outcome=WhiteGlovePart2Complete, whiteglove_part2_complete event emitted.
|
||||
# Plan: §4 M3.4.
|
||||
{"SessionSignalOrdinal":0,"SessionTraceOrdinal":0,"Kind":"SessionStarted","KindSchemaVersion":1,"OccurredAtUtc":"2026-04-23T08:00:00Z","SourceOrigin":"orchestrator","Evidence":{"Kind":"Synthetic","Identifier":"session:wg-part2-happy:started","Summary":"Session start anchor"}}
|
||||
{"SessionSignalOrdinal":1,"SessionTraceOrdinal":1,"Kind":"EspPhaseChanged","KindSchemaVersion":1,"OccurredAtUtc":"2026-04-23T08:01:00Z","SourceOrigin":"EspAndHelloTracker","Evidence":{"Kind":"Derived","Identifier":"esp-phase-detector-v1","Summary":"ESP phase=DeviceSetup","DerivationInputs":{"rawValue":"1"}},"Payload":{"phase":"DeviceSetup"}}
|
||||
{"SessionSignalOrdinal":2,"SessionTraceOrdinal":2,"Kind":"WhiteGloveShellCoreSuccess","KindSchemaVersion":1,"OccurredAtUtc":"2026-04-23T08:02:00Z","SourceOrigin":"ShellCoreTracker","Evidence":{"Kind":"Raw","Identifier":"eventlog:Shell-Core:62407:recordId:310","Summary":"Shell Core white-glove success"}}
|
||||
{"SessionSignalOrdinal":3,"SessionTraceOrdinal":3,"Kind":"SessionRecovered","KindSchemaVersion":1,"OccurredAtUtc":"2026-04-23T08:07:00Z","SourceOrigin":"orchestrator","Evidence":{"Kind":"Synthetic","Identifier":"session:wg-part2-happy:recovered","Summary":"Agent started after reboot; Part 1 sealed state persisted"}}
|
||||
{"SessionSignalOrdinal":4,"SessionTraceOrdinal":4,"Kind":"UserAadSignInComplete","KindSchemaVersion":1,"OccurredAtUtc":"2026-04-23T08:10:00Z","SourceOrigin":"AadJoinWatcher","Evidence":{"Kind":"Derived","Identifier":"aad-join-watcher-v1","Summary":"AAD user sign-in complete","DerivationInputs":{"user":"user-anon-0015"}}}
|
||||
{"SessionSignalOrdinal":5,"SessionTraceOrdinal":5,"Kind":"HelloResolvedPart2","KindSchemaVersion":1,"OccurredAtUtc":"2026-04-23T08:11:00Z","SourceOrigin":"EspAndHelloTracker","Evidence":{"Kind":"Raw","Identifier":"hello-event-recordId:211","Summary":"Hello for Business enrolled (Part 2)"},"Payload":{"outcome":"Success"}}
|
||||
{"SessionSignalOrdinal":6,"SessionTraceOrdinal":6,"Kind":"DesktopArrivedPart2","KindSchemaVersion":1,"OccurredAtUtc":"2026-04-23T08:12:00Z","SourceOrigin":"DesktopArrivalDetector","Evidence":{"Kind":"Derived","Identifier":"desktop-arrival-detector-v1","Summary":"Explorer.exe running as real user (Part 2)","DerivationInputs":{"user":"user-anon-0015","explorerPid":"4300"}}}
|
||||
{"SessionSignalOrdinal":7,"SessionTraceOrdinal":7,"Kind":"AccountSetupCompletedPart2","KindSchemaVersion":1,"OccurredAtUtc":"2026-04-23T08:13:00Z","SourceOrigin":"EspAndHelloTracker","Evidence":{"Kind":"Derived","Identifier":"account-setup-complete-detector-v1","Summary":"Account setup phase marked complete (Part 2)","DerivationInputs":{"registryKey":"HKLM\\...EspStatus\\AccountSetup","rawValue":"Complete"}}}
|
||||
@@ -0,0 +1,11 @@
|
||||
# Scenario: WhiteGlove Part 2 Stuck (user never signs in; 24h safety deadline fires)
|
||||
# Session seals Part 1, reboots, and waits for the user — who never signs in. The
|
||||
# 24h WhiteGlovePart2Safety deadline fires -> Stage=Failed, Outcome=EnrollmentFailed,
|
||||
# Part2Completion hypothesis Rejected with reason "part2_user_absent",
|
||||
# enrollment_failed event emitted with reason="part2_user_absent".
|
||||
# Plan: §4 M3.4.
|
||||
{"SessionSignalOrdinal":0,"SessionTraceOrdinal":0,"Kind":"SessionStarted","KindSchemaVersion":1,"OccurredAtUtc":"2026-04-23T09:00:00Z","SourceOrigin":"orchestrator","Evidence":{"Kind":"Synthetic","Identifier":"session:wg-part2-stuck:started","Summary":"Session start anchor"}}
|
||||
{"SessionSignalOrdinal":1,"SessionTraceOrdinal":1,"Kind":"EspPhaseChanged","KindSchemaVersion":1,"OccurredAtUtc":"2026-04-23T09:01:00Z","SourceOrigin":"EspAndHelloTracker","Evidence":{"Kind":"Derived","Identifier":"esp-phase-detector-v1","Summary":"ESP phase=DeviceSetup","DerivationInputs":{"rawValue":"1"}},"Payload":{"phase":"DeviceSetup"}}
|
||||
{"SessionSignalOrdinal":2,"SessionTraceOrdinal":2,"Kind":"WhiteGloveShellCoreSuccess","KindSchemaVersion":1,"OccurredAtUtc":"2026-04-23T09:02:00Z","SourceOrigin":"ShellCoreTracker","Evidence":{"Kind":"Raw","Identifier":"eventlog:Shell-Core:62407:recordId:320","Summary":"Shell Core white-glove success"}}
|
||||
{"SessionSignalOrdinal":3,"SessionTraceOrdinal":3,"Kind":"SessionRecovered","KindSchemaVersion":1,"OccurredAtUtc":"2026-04-23T09:07:00Z","SourceOrigin":"orchestrator","Evidence":{"Kind":"Synthetic","Identifier":"session:wg-part2-stuck:recovered","Summary":"Agent started after reboot"}}
|
||||
{"SessionSignalOrdinal":4,"SessionTraceOrdinal":4,"Kind":"DeadlineFired","KindSchemaVersion":1,"OccurredAtUtc":"2026-04-24T09:07:00Z","SourceOrigin":"DeadlineScheduler","Evidence":{"Kind":"Synthetic","Identifier":"deadline:whiteglove_part2_safety:fired","Summary":"Part 2 safety deadline fired — no user sign-in within 24 h"},"Payload":{"deadline":"whiteglove_part2_safety"}}
|
||||
Reference in New Issue
Block a user