diff --git a/src/Agent/AutopilotMonitor.Agent.V2.Core.Tests/SignalAdapters/RealmJoinWatcherAdapterTests.cs b/src/Agent/AutopilotMonitor.Agent.V2.Core.Tests/SignalAdapters/RealmJoinWatcherAdapterTests.cs
index 4ad27ab0..5d9ec9a8 100644
--- a/src/Agent/AutopilotMonitor.Agent.V2.Core.Tests/SignalAdapters/RealmJoinWatcherAdapterTests.cs
+++ b/src/Agent/AutopilotMonitor.Agent.V2.Core.Tests/SignalAdapters/RealmJoinWatcherAdapterTests.cs
@@ -224,6 +224,64 @@ namespace AutopilotMonitor.Agent.V2.Core.Tests.SignalAdapters
Assert.Equal("0", info.Payload["lastExitCode"]);
}
+ [Fact]
+ public void Package_watchers_stay_disarmed_until_phase_reaches_RunningThreshold()
+ {
+ // Pre-RJ window: IME may still be writing to HKLM\SOFTWARE\RealmJoin while the
+ // RJ service key is already present but DeploymentPhase is still Blank (0).
+ // Package watchers must NOT arm yet — otherwise IME's in-flight package writes
+ // would be misattributed to RJ's lifecycle.
+ using var f = new Fixture();
+ using var adapter = new RealmJoinWatcherAdapter(f.Watcher, f.Ingress, f.Clock);
+
+ // Service-appearance fires Detected without a phase value — must NOT arm.
+ f.Watcher.TriggerNotifyRealmJoinPresenceFromTest(phase: null);
+ Assert.False(f.Watcher.PackageWatchersArmedForTest);
+
+ // Phase = 0 (Blank) — still no arm; RJ is installed but not deploying.
+ f.Watcher.TriggerNotifyRealmJoinPresenceFromTest(phase: 0);
+ Assert.False(f.Watcher.PackageWatchersArmedForTest);
+
+ // Phase transitions to 100 (RunningFirstDeployment) — package watchers MUST arm now.
+ f.Watcher.TriggerNotifyRealmJoinPresenceFromTest(phase: 100);
+ Assert.True(f.Watcher.PackageWatchersArmedForTest);
+ }
+
+ [Fact]
+ public void Package_watchers_stay_armed_once_phase_subsequently_drops_back_below_threshold()
+ {
+ // Set-once semantics — even if a later phase reading were below threshold
+ // (defensive against partial / racy registry writes), the watcher does not
+ // disarm and re-introduce a window where post-running events could be lost.
+ using var f = new Fixture();
+ using var adapter = new RealmJoinWatcherAdapter(f.Watcher, f.Ingress, f.Clock);
+
+ f.Watcher.TriggerNotifyRealmJoinPresenceFromTest(phase: 100);
+ Assert.True(f.Watcher.PackageWatchersArmedForTest);
+
+ // Hypothetical regression: phase reads as 0 again — must not flip the flag.
+ f.Watcher.TriggerNotifyRealmJoinPresenceFromTest(phase: 0);
+ Assert.True(f.Watcher.PackageWatchersArmedForTest);
+ }
+
+ [Fact]
+ public void Package_watchers_arm_immediately_when_first_phase_observed_is_completed()
+ {
+ // Agent boots into a session where RJ is already at CompletedFirstDeployment
+ // (110) — e.g. pre-installed on an image, or recovered after a long absence.
+ // The first phase observation is already past threshold, so package watchers
+ // arm in that same notify pass; the historic package rows enumerated by
+ // CheckMachinePackages then surface as started+completed pairs.
+ using var f = new Fixture();
+ using var adapter = new RealmJoinWatcherAdapter(f.Watcher, f.Ingress, f.Clock);
+
+ f.Watcher.TriggerNotifyRealmJoinPresenceFromTest(phase: 110);
+
+ Assert.True(f.Watcher.PackageWatchersArmedForTest);
+ // Resolved fires on the same pass.
+ Assert.Contains(f.Ingress.Posted, p => p.Kind == DecisionSignalKind.RealmJoinResolved);
+ }
+
[Fact]
public void Watcher_fires_PackageStarted_even_when_DisplayName_is_missing()
{
diff --git a/src/Agent/AutopilotMonitor.Agent.V2.Core/Monitoring/Enrollment/SystemSignals/RealmJoinInfo.cs b/src/Agent/AutopilotMonitor.Agent.V2.Core/Monitoring/Enrollment/SystemSignals/RealmJoinInfo.cs
index d133b5df..23c7e4e6 100644
--- a/src/Agent/AutopilotMonitor.Agent.V2.Core/Monitoring/Enrollment/SystemSignals/RealmJoinInfo.cs
+++ b/src/Agent/AutopilotMonitor.Agent.V2.Core/Monitoring/Enrollment/SystemSignals/RealmJoinInfo.cs
@@ -66,6 +66,14 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Enrollment.SystemSignals
// Resolved-phase value per the RJ enum (RealmJoin.Core.SoftwarePackaging.DeploymentPhase).
public const int PhaseCompletedFirstDeployment = 110;
+ ///
+ /// Minimum DWORD value indicating RJ has moved out of Blank (0) into an active
+ /// or terminal deployment phase. Below this the SOFTWARE\RealmJoin subtree may still
+ /// be written by adjacent IME packages — package watchers stay disarmed until RJ owns
+ /// the keys.
+ ///
+ public const int PhaseRunningThresholdMin = 100;
+
///
/// Map a raw DeploymentPhase DWORD to its RJ-internal enum name. Mirrors
/// RealmJoin.Core.SoftwarePackaging.DeploymentPhase as documented at feature
diff --git a/src/Agent/AutopilotMonitor.Agent.V2.Core/Monitoring/Enrollment/SystemSignals/RealmJoinWatcher.cs b/src/Agent/AutopilotMonitor.Agent.V2.Core/Monitoring/Enrollment/SystemSignals/RealmJoinWatcher.cs
index b8e1440f..ea166329 100644
--- a/src/Agent/AutopilotMonitor.Agent.V2.Core/Monitoring/Enrollment/SystemSignals/RealmJoinWatcher.cs
+++ b/src/Agent/AutopilotMonitor.Agent.V2.Core/Monitoring/Enrollment/SystemSignals/RealmJoinWatcher.cs
@@ -118,10 +118,11 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Enrollment.SystemSignals
private readonly HashSet _hkuPackagesCompleted = new HashSet(StringComparer.OrdinalIgnoreCase);
private string? _hkuSid;
- // Set once the Services\realmjoin gate fires. Until then, HKLM\SOFTWARE\RealmJoin and
- // HKU\\SOFTWARE\RealmJoin remain unarmed — RJ presence is the prerequisite for
- // looking at those keys at all.
- private bool _realmJoinServiceDetected;
+ // Set once the first non-Blank DeploymentPhase (>= 100) is observed. Package watchers
+ // (HKLM\SOFTWARE\RealmJoin + HKU\\SOFTWARE\RealmJoin) only arm at that point —
+ // earlier observation would mix IME's in-flight package writes (which share the same
+ // subtree) into RJ's lifecycle.
+ private bool _packageWatchersArmed;
private bool _disposed;
public event EventHandler? RealmJoinDetected;
@@ -151,10 +152,11 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Enrollment.SystemSignals
/// resolves a real user owner and
/// produces the SID. Idempotent.
///
- /// The actual HKU watcher only arms once the Services\realmjoin gate fires —
- /// either it has already fired (immediate arm) or it fires later
- /// (deferred arm from ). On devices
- /// without RJ the SID is recorded but no HKU watcher is ever created.
+ /// The actual HKU watcher only arms once the package-watcher arming threshold has
+ /// fired (RJ observed at or
+ /// higher). Either it has already fired (immediate arm) or fires later
+ /// (deferred arm from ). On devices without RJ
+ /// or where RJ never leaves Blank, the SID is recorded but no HKU watcher is created.
///
///
public void ArmHku(string sid)
@@ -167,7 +169,7 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Enrollment.SystemSignals
{
if (_hkuSid != null) return;
_hkuSid = sid;
- armNow = _realmJoinServiceDetected;
+ armNow = _packageWatchersArmed;
}
_logger.Info($"RealmJoinWatcher: recorded SID {sid} for HKU watching (armNow={armNow})");
@@ -337,7 +339,6 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Enrollment.SystemSignals
private void AttachServiceRealmjoinSubtreeWatcher()
{
- bool armUserToo;
lock (_stateLock)
{
if (_serviceRealmjoinWatcher != null) return;
@@ -361,23 +362,16 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Enrollment.SystemSignals
{
_logger.Warning($"RealmJoinWatcher: failed to attach Services\\realmjoin watcher: {ex.Message}");
}
-
- _realmJoinServiceDetected = true;
- armUserToo = _hkuSid != null;
}
// The appearance of Services\realmjoin is itself the RJ-detected signal —
- // surface it even if Parameters\DeploymentPhase isn't populated yet.
+ // surface it even if Parameters\DeploymentPhase isn't populated yet. The package
+ // watchers (HKLM + HKU SOFTWARE\RealmJoin) intentionally stay disarmed here:
+ // NotifyRealmJoinPresence arms them once DeploymentPhase >= PhaseRunningThresholdMin
+ // is observed. Before that the same subtree may still receive in-flight IME
+ // package writes that are not part of RJ's lifecycle.
NotifyRealmJoinPresence(phase: null);
CheckParameters();
-
- // RJ is now confirmed installed — arm the package watchers.
- EnsureMachineRealmJoinStage();
- if (armUserToo)
- {
- CheckUserPackages();
- EnsureUserRealmJoinStage();
- }
}
private void AttachMachineRealmJoinSubtreeWatcher()
@@ -469,6 +463,8 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Enrollment.SystemSignals
bool firePhaseChange = false;
bool fireResolved = false;
int? prevPhase = null;
+ bool armPackageWatchers = false;
+ bool armUserPackageWatcherToo = false;
lock (_stateLock)
{
@@ -488,6 +484,15 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Enrollment.SystemSignals
fireResolved = true;
_resolvedFired = true;
}
+ // Arm the package watchers once RJ leaves Blank (phase >= 100). Before that,
+ // IME may still be writing to HKLM\SOFTWARE\RealmJoin during the pre-RJ
+ // install window and we'd misattribute those rows to RJ's lifecycle.
+ if (!_packageWatchersArmed && phase.Value >= RealmJoinInfo.PhaseRunningThresholdMin)
+ {
+ _packageWatchersArmed = true;
+ armPackageWatchers = true;
+ armUserPackageWatcherToo = _hkuSid != null;
+ }
}
}
@@ -515,6 +520,17 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Enrollment.SystemSignals
try { RealmJoinResolved?.Invoke(this, new RealmJoinResolvedEventArgs(phase.Value)); }
catch (Exception ex) { _logger.Error("RealmJoinWatcher: RealmJoinResolved handler threw", ex); }
}
+
+ if (armPackageWatchers)
+ {
+ _logger.Info($"RealmJoinWatcher: phase={phase} crossed RunningThreshold — arming package watchers (HKLM + HKU={armUserPackageWatcherToo})");
+ EnsureMachineRealmJoinStage();
+ if (armUserPackageWatcherToo)
+ {
+ CheckUserPackages();
+ EnsureUserRealmJoinStage();
+ }
+ }
}
private void CheckMachinePackages()
@@ -649,5 +665,12 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Enrollment.SystemSignals
internal void TriggerUserPackageObservationFromTest(RealmJoinPackageSnapshot snap) =>
MaybeFirePackageEvents(RealmJoinPackageScope.User, snap, _hkuPackagesStarted, _hkuPackagesCompleted);
+
+ internal void TriggerNotifyRealmJoinPresenceFromTest(int? phase) => NotifyRealmJoinPresence(phase);
+
+ internal bool PackageWatchersArmedForTest
+ {
+ get { lock (_stateLock) { return _packageWatchersArmed; } }
+ }
}
}