feat(agent-v2): detect pre-installed OEM/consumer Office, kill false office_install_failed

The OfficeInstallDetector fired a false office_install_failed on devices with
pre-installed OEM/consumer inbox Office (e.g. O365HomePremRetail +
OneNoteFreeRetail) running a background C2R CLIENTUPDATE/maintenance scenario:
the already-resident Office was mis-read as a failed Microsoft 365 Apps install,
with a benign small-integer scenario value ("2") treated as a failure code.

Field telemetry (202 office_install_started events) showed 200/202 real installs
start with streamingFinished=false (a live download) and only 2 already-resident
— so the clean discriminator is "already on disk", not the scenario name. A
"require INSTALL scenario to start" gate was rejected: 47% of real starts fire
via the early DO-CDN job before C2R writes its registry.

Changes:
- New office_preinstalled_detected (Info): emitted when the core Office binaries
  are already on disk at the first signal AND the product set is consumer/OEM
  inbox (OfficeProductClassifier). Anchored on the filesystem, NOT the flaky
  StreamingFinished value (absent on some machines). Enterprise Office already on
  disk keeps the started->completed path (CSP/Win32-wrapper case).
- Does NOT latch terminal: an enrollment often uninstalls the inbox Office and
  installs a fresh one, so the detector stays armed (emit-once guard +
  StatePreinstalled persisted for cross-reboot dedup, ResumePreinstalled set
  before the process watcher's startup probe).
- Error attribution scoped to the INSTALL scenario only (+ errorSource); a
  CLIENTUPDATE error never fails an install.
- Registry idle-start gated on InstallScenarioPresent (not any scenario).
- IsNonZeroNumericCode rejects implausibly small positive decimals (<1000).
- DO bytes are seeded only once a real lifecycle opens, so a preinstalled/
  CLIENTUPDATE download cannot leak into a later install's doSummary.
- Web InstallProgress renders a sky "Pre-installed" info row; MCP + web event
  catalogs updated.

V2.Core 1454/1454, web tsc clean, MCP 240 passed. Inert until redeploy.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Oliver Kieselbach
2026-06-15 21:34:08 +02:00
parent edfb6bddc2
commit cd29cb86c1
12 changed files with 473 additions and 30 deletions
@@ -53,7 +53,7 @@ namespace AutopilotMonitor.Agent.V2.Core.Tests.Monitoring.Office
public List<FakeSignalIngressSink.PostedSignal> OfficeEvents() => Sink.Posted
.Where(p => p.Payload != null
&& p.Payload.TryGetValue(SignalPayloadKeys.EventType, out var et)
&& et != null && et.StartsWith("office_install_", StringComparison.Ordinal))
&& et != null && et.StartsWith("office_", StringComparison.Ordinal))
.ToList();
public void Dispose() => _tmp.Dispose();
@@ -69,6 +69,8 @@ namespace AutopilotMonitor.Agent.V2.Core.Tests.Monitoring.Office
StreamingFinished = false,
ActiveScenarioPresent = true,
ActiveScenarioName = "INSTALL",
InstallScenarioPresent = true,
InstallScenarioName = "INSTALL",
OfficeC2RClientRunning = true,
};
s.Products.Add("O365ProPlusRetail");
@@ -92,6 +94,28 @@ namespace AutopilotMonitor.Agent.V2.Core.Tests.Monitoring.Office
return s;
}
// An Office that is already fully resident on disk at the first signal: OEM/consumer inbox
// Office (O365HomePremRetail + OneNoteFreeRetail) running a background CLIENTUPDATE — the
// false-positive shape from field session fa526757.
private static OfficeC2RSnapshot PreinstalledConsumer(string scenario = "CLIENTUPDATE")
{
var s = new OfficeC2RSnapshot
{
ConfigurationKeyPresent = true,
Channel = "Current",
Platform = "x64",
StreamingFinished = true,
ActiveScenarioPresent = true,
ActiveScenarioName = scenario,
OfficeC2RClientRunning = true,
InstallationPath = @"C:\Program Files\Microsoft Office",
VersionToReport = "16.0.16327.20264",
};
s.Products.Add("O365HomePremRetail");
s.Products.Add("OneNoteFreeRetail");
return s;
}
private static string EventType(FakeSignalIngressSink.PostedSignal p) => p.Payload![SignalPayloadKeys.EventType];
private static string Severity(FakeSignalIngressSink.PostedSignal p) => p.Payload![SignalPayloadKeys.Severity];
private static Dictionary<string, object> Data(FakeSignalIngressSink.PostedSignal p)
@@ -382,12 +406,188 @@ namespace AutopilotMonitor.Agent.V2.Core.Tests.Monitoring.Office
Assert.Equal("0x80070005", Data(failed)["errorCode"]);
}
// ---------------------------------------------------------------- preinstalled (already-resident)
[Fact]
public void Resident_consumer_office_emits_preinstalled_not_started_or_failed()
{
// OEM/consumer inbox Office already on disk at the first signal, running a background
// CLIENTUPDATE — surface an informational office_preinstalled_detected, never a
// started/failed pair (field session fa526757 / the CLIENTUPDATE errorCode "2" false alarm).
using var rig = new Rig(PreinstalledConsumer());
rig.Sut.CoreBinariesProbe = _ => true; // core Office binaries already present on disk
rig.Sut.OnWorkerStarted();
var ev = Assert.Single(rig.OfficeEvents());
Assert.Equal(Constants.EventTypes.OfficePreinstalledDetected, EventType(ev));
Assert.Equal("Info", Severity(ev));
var data = Data(ev);
Assert.Equal("office_already_resident", data["reason"]);
Assert.Equal("CLIENTUPDATE", data["scenario"]);
Assert.Contains("O365HomePremRetail", (List<string>)data["products"]);
}
[Fact]
public void Resident_consumer_office_with_install_scenario_still_preinstalled()
{
// The real field false positive had scenario=INSTALL yet Office was already resident — the
// filesystem proof (not the scenario name) is what classifies it as preinstalled.
using var rig = new Rig(PreinstalledConsumer(scenario: "INSTALL"));
rig.Sut.CoreBinariesProbe = _ => true;
rig.Sut.OnWorkerStarted();
Assert.Equal(Constants.EventTypes.OfficePreinstalledDetected, EventType(Assert.Single(rig.OfficeEvents())));
}
[Fact]
public void Resident_enterprise_office_still_starts_not_preinstalled()
{
// Enterprise Office already on disk (Office CSP / Win32-wrapper re-run, field session
// a7525e97) is NOT preinstalled inbox Office — keep the started→completed path.
var snap = ActiveStreaming(); // O365ProPlusRetail
snap.InstallationPath = @"C:\Program Files\Microsoft Office";
using var rig = new Rig(snap);
rig.Sut.CoreBinariesProbe = _ => true;
rig.Sut.OnWorkerStarted();
Assert.Equal(Constants.EventTypes.OfficeInstallStarted, EventType(Assert.Single(rig.OfficeEvents())));
}
[Fact]
public void Fresh_install_without_binaries_on_disk_starts_normally()
{
// The common case (200/202 field starts): early DO trigger, nothing on disk yet → started.
using var rig = new Rig(PreinstalledConsumer());
rig.Sut.CoreBinariesProbe = _ => false; // no binaries on disk yet → a genuine fresh install
rig.Sut.OnWorkerStarted();
Assert.Equal(Constants.EventTypes.OfficeInstallStarted, EventType(Assert.Single(rig.OfficeEvents())));
}
[Fact]
public void Preinstalled_persists_nonterminal_state_so_next_run_rearms()
{
// Persisted for emit-once dedup, but NOT terminal — the factory must still arm the detector
// next run (an enrollment often uninstalls the inbox Office and installs a fresh one).
using var rig = new Rig(PreinstalledConsumer(), withPersistence: true);
rig.Sut.CoreBinariesProbe = _ => true;
rig.Sut.OnWorkerStarted();
var state = rig.Persistence!.Load();
Assert.Equal(OfficeInstallStateData.StatePreinstalled, state!.State);
Assert.False(state.IsTerminal);
}
[Fact]
public void Preinstalled_emits_once_then_a_later_fresh_install_still_starts()
{
// Resident inbox Office reports preinstalled (no lifecycle). Then the enrollment lays down a
// fresh enterprise Microsoft 365 Apps (real download, no binaries on disk yet) — that MUST
// still open a normal started lifecycle (the re-arm requirement).
using var rig = new Rig(PreinstalledConsumer());
rig.Sut.CoreBinariesProbe = _ => true;
rig.Sut.OnWorkerStarted(); // → office_preinstalled_detected, stays Idle/armed
// A second resident-consumer signal must NOT emit a duplicate preinstalled.
rig.Sut.OnWorkerStarted();
var afterFirst = rig.OfficeEvents();
Assert.Single(afterFirst);
Assert.Equal(Constants.EventTypes.OfficePreinstalledDetected, EventType(afterFirst[0]));
// Now a fresh enterprise install begins (binaries not yet on disk → real download).
rig.Current = ActiveStreaming(); // O365ProPlusRetail, streaming
rig.Sut.CoreBinariesProbe = _ => false;
rig.Sut.OnOfficeDoSample(new OfficeDoSample { JobCount = 2, FileSize = 1000, TotalBytesDownloaded = 100 });
var events = rig.OfficeEvents();
Assert.Equal(2, events.Count);
Assert.Equal(Constants.EventTypes.OfficeInstallStarted, EventType(events[1]));
}
[Fact]
public void Preinstalled_do_bytes_do_not_leak_into_a_later_real_install_summary()
{
// A CLIENTUPDATE DO download (large bytes) drives the preinstalled short-circuit — those
// bytes must NOT seed _peakDo, or a later real install's doSummary would report them.
using var rig = new Rig(PreinstalledConsumer());
rig.Sut.CoreBinariesProbe = _ => true;
rig.Sut.OnOfficeDoSample(new OfficeDoSample { JobCount = 3, FileSize = 5000, TotalBytesDownloaded = 4000 });
Assert.Equal(Constants.EventTypes.OfficePreinstalledDetected, EventType(Assert.Single(rig.OfficeEvents())));
// Now a real enterprise install begins (smaller download) and completes.
rig.Current = ActiveStreaming();
rig.Sut.CoreBinariesProbe = _ => false;
rig.Sut.OnOfficeDoSample(new OfficeDoSample { JobCount = 1, FileSize = 1000, TotalBytesDownloaded = 600, BytesFromHttp = 600 });
rig.Current = CompletedIdle();
rig.Sut.CoreBinariesProbe = _ => true;
Assert.Equal(OfficeInstallDetector.CompletionOutcome.Completed, rig.Sut.TryFinalizeCompletion());
var completed = rig.OfficeEvents().Last();
Assert.Equal(Constants.EventTypes.OfficeInstallCompleted, EventType(completed));
var summary = Assert.IsType<Dictionary<string, object>>(Data(completed)["doSummary"]);
Assert.Equal(600L, summary["totalBytesDownloaded"]); // the real install's bytes, NOT the 4000 CLIENTUPDATE bytes
}
[Fact]
public void ResumePreinstalled_suppresses_duplicate_emit_but_stays_armed()
{
using var rig = new Rig(PreinstalledConsumer());
rig.Sut.CoreBinariesProbe = _ => true;
// Simulate a restart where preinstalled was already reported in a previous run.
rig.Sut.ResumePreinstalled(new OfficeInstallStateData { State = OfficeInstallStateData.StatePreinstalled });
// Resident-consumer signal this run must NOT re-emit preinstalled.
rig.Sut.OnWorkerStarted();
Assert.Empty(rig.OfficeEvents());
// But a fresh enterprise install still starts (armed).
rig.Current = ActiveStreaming();
rig.Sut.CoreBinariesProbe = _ => false;
rig.Sut.OnWorkerStarted();
Assert.Equal(Constants.EventTypes.OfficeInstallStarted, EventType(Assert.Single(rig.OfficeEvents())));
}
// ---------------------------------------------------------------- product classifier
[Theory]
[InlineData(true, "O365HomePremRetail", "OneNoteFreeRetail")] // OEM consumer inbox
[InlineData(true, "OneNoteFreeRetail")]
[InlineData(true, "HomeBusinessRetail")] // consumer perpetual — NOT the managed O365Business
[InlineData(true, "HomeStudent2019Retail")]
[InlineData(false, "O365ProPlusRetail")] // M365 Apps for enterprise
[InlineData(false, "O365BusinessRetail")] // M365 Apps for business (managed)
[InlineData(false, "O365HomePremRetail", "O365ProPlusRetail")] // mixed → managed wins
[InlineData(false, "SomeUnknownSku")] // unknown → not consumer
public void IsConsumerInboxProductSet_classifies_consumer_vs_managed(bool expected, params string[] products)
{
Assert.Equal(expected, OfficeProductClassifier.IsConsumerInboxProductSet(products));
}
[Fact]
public void IsConsumerInboxProductSet_empty_is_not_consumer()
{
Assert.False(OfficeProductClassifier.IsConsumerInboxProductSet(new List<string>()));
Assert.False(OfficeProductClassifier.IsConsumerInboxProductSet(null));
}
// ---------------------------------------------------------------- error-code heuristic
[Theory]
[InlineData("0x80070005", true)] // hex HRESULT
[InlineData("17002", true)] // decimal C2R exit code
[InlineData("30015", true)] // decimal C2R exit code
[InlineData("-2147024891", true)] // negative decimal HRESULT
[InlineData("2", false)] // implausibly small positive — a state/enum, not an error
[InlineData("9", false)]
[InlineData("999", false)] // just below the plausibility floor
[InlineData("1000", true)] // at the floor
[InlineData("0x0", false)] // zero hex
[InlineData("0", false)] // zero decimal
[InlineData("Success", false)] // benign textual value (e.g. Result=Success)
@@ -395,7 +595,7 @@ namespace AutopilotMonitor.Agent.V2.Core.Tests.Monitoring.Office
[InlineData("InProgress", false)]
[InlineData("", false)]
[InlineData(null, false)]
public void IsNonZeroNumericCode_only_treats_nonzero_numbers_as_errors(string? value, bool expected)
public void IsNonZeroNumericCode_only_treats_plausible_nonzero_numbers_as_errors(string? value, bool expected)
{
Assert.Equal(expected, OfficeInstallDetector.IsNonZeroNumericCode(value!));
}
@@ -96,6 +96,7 @@ namespace AutopilotMonitor.Agent.V2.Core.Tests.Monitoring.Office
[InlineData(OfficeInstallStateData.StateActive, false)]
[InlineData(OfficeInstallStateData.StateCompleted, true)]
[InlineData(OfficeInstallStateData.StateFailed, true)]
[InlineData(OfficeInstallStateData.StatePreinstalled, false)] // persisted for dedup, but re-arms
public void IsTerminal_only_for_completed_and_failed(string state, bool expected)
{
Assert.Equal(expected, new OfficeInstallStateData { State = state }.IsTerminal);
@@ -59,6 +59,14 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Telemetry.Office
// names against a field enrollment and widen only if needed.
private static readonly string[] ErrorValueHints = { "errorcode", "hresult", "exitcode", "lasterror", "failurecode" };
// Install-class C2R scenario subkey names. Only these anchor an error code (a failure in a
// CLIENTUPDATE / UPDATE / maintenance scenario must not fail an install). Deliberately narrow
// — the Autopilot enrollment cares about the fresh product lay-down, not client self-updates.
private static readonly string[] InstallScenarioNames = { "INSTALL" };
private static bool IsInstallScenario(string? name)
=> name != null && InstallScenarioNames.Any(s => string.Equals(s, name, StringComparison.OrdinalIgnoreCase));
// Core Office app binaries used as the on-disk completion proof. ANY of these present under
// {InstallationPath}\root\* means the C2R lay-down finished — "any of", because a deployment can
// exclude products (e.g. no Outlook), so requiring all would miss legitimate completions.
@@ -93,6 +101,7 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Telemetry.Office
internal enum CompletionOutcome { Completed, Failed, NotYet }
private DetectorState _state = DetectorState.Idle;
private bool _preinstalledReported; // office_preinstalled_detected already emitted (emit-once guard)
private DateTime? _startedAtUtc;
private string? _startedTrigger;
private OfficeDoSample? _lastDo;
@@ -141,6 +150,22 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Telemetry.Office
}
}
/// <summary>
/// Restores the "preinstalled already reported" guard after an agent restart so resident inbox
/// Office does not re-emit <c>office_preinstalled_detected</c> every reboot — while leaving the
/// detector Idle/armed so a later fresh (enterprise) install in this enrollment is still caught.
/// </summary>
internal void ResumePreinstalled(OfficeInstallStateData state)
{
if (state == null || state.State != OfficeInstallStateData.StatePreinstalled) return;
lock (_lock)
{
if (_state != DetectorState.Idle) return;
_preinstalledReported = true;
_logger.Info($"[{SourceName}] resumed preinstalled-reported guard (armed for a later fresh install)");
}
}
/// <summary>
/// Surfaces the C2R <c>InstallationPath</c> to the host (once) as soon as it is known, so the
/// host can arm the <see cref="OfficeBinaryWatcher"/> on the correct tree. Caller holds the lock.
@@ -189,7 +214,11 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Telemetry.Office
if (_state == DetectorState.Idle)
{
if (snap.ActiveScenarioPresent) BeginIfIdle("registry", snap);
// Start from the registry ONLY for an install-class scenario. A CLIENTUPDATE / UPDATE
// scenario key (the C2R client self-update / maintenance) must not open an install
// lifecycle. The early DO-job trigger (OnOfficeDoSample) still catches a real install
// before its INSTALL key is written — the common case (47% of field starts).
if (snap.InstallScenarioPresent) BeginIfIdle("registry", snap);
return;
}
EvaluateActive(snap);
@@ -209,9 +238,6 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Telemetry.Office
if (sample == null) return;
lock (_lock)
{
_lastDo = sample;
var peakGrew = _peakDo == null || sample.TotalBytesDownloaded > _peakDo.TotalBytesDownloaded;
if (peakGrew) _peakDo = sample;
if (_state == DetectorState.Terminal) return;
if (_state == DetectorState.Idle)
@@ -220,11 +246,24 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Telemetry.Office
var snapStart = ReadSnapshotSafe();
if (snapStart == null) return;
BeginIfIdle("do", snapStart);
// Seed the DO summary from the triggering sample ONLY if a real lifecycle actually
// opened. A preinstalled short-circuit stays Idle — capturing its CLIENTUPDATE bytes
// here would leak them into a LATER real install's doSummary (which is built from
// _peakDo). The seed is persisted directly (it is the lifecycle's first DO sample).
if (_state == DetectorState.Active)
{
_lastDo = sample;
_peakDo = sample;
PersistState(OfficeInstallStateData.StateActive);
}
return;
}
// Active: no progress event — the sample only updates the DO summary data. A grown
// peak is re-persisted (throttled) so a mid-install agent restart keeps the doSummary.
_lastDo = sample;
var peakGrew = _peakDo == null || sample.TotalBytesDownloaded > _peakDo.TotalBytesDownloaded;
if (peakGrew) _peakDo = sample;
if (peakGrew && _statePersistence != null
&& (!_lastStatePersistUtc.HasValue
|| (_clock.UtcNow - _lastStatePersistUtc.Value).TotalSeconds >= PeakPersistThrottleSeconds))
@@ -294,6 +333,37 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Telemetry.Office
private void BeginIfIdle(string trigger, OfficeC2RSnapshot snap)
{
if (_state != DetectorState.Idle) return;
// Already-resident detection — the precision gate. When the core Office binaries are
// ALREADY on disk at the very first signal, this C2R activity is NOT a fresh install:
// Office was laid down before the detector opened. Two flavours:
// * consumer / OEM-inbox product set → surface an informational office_preinstalled_detected
// (the C2R client running a background CLIENTUPDATE on pre-provisioned Office — field
// session fa526757), NOT a started/failed pair.
// * enterprise product set → fall through to the existing started→completed path (Office
// CSP / Win32-wrapper re-run on already-present Office — field session a7525e97).
// Anchored on the filesystem, NOT the StreamingFinished registry value (absent on some
// machines → reads false), so it holds even when that value is missing.
//
// Crucially this does NOT latch a terminal: an enrollment commonly UNINSTALLS the inbox
// Office and lays down a fresh (enterprise) Microsoft 365 Apps afterwards, which we must
// still report. So we emit the informational fact ONCE (idempotent guard) and stay Idle —
// a later fresh install (enterprise product set, or a real download with no binaries on
// disk yet) opens a normal started→completed lifecycle from here.
if (ProbeCoreBinaries(snap.InstallationPath)
&& OfficeProductClassifier.IsConsumerInboxProductSet(snap.Products))
{
if (!_preinstalledReported)
{
_preinstalledReported = true;
_startedAtUtc = _clock.UtcNow;
_startedTrigger = trigger;
EmitPreinstalled(snap);
PersistState(OfficeInstallStateData.StatePreinstalled);
}
return; // stay Idle (armed) — never open an install lifecycle for resident inbox Office
}
_state = DetectorState.Active;
_startedAtUtc = _clock.UtcNow;
_startedTrigger = trigger;
@@ -330,6 +400,35 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Telemetry.Office
PersistState(OfficeInstallStateData.StateFailed);
}
/// <summary>
/// Emits the informational <c>office_preinstalled_detected</c> for an Office that was already
/// resident on disk at the first signal (OEM/consumer inbox Office running a background
/// CLIENTUPDATE/maintenance scenario). This is NOT an enrollment install — Info severity, no
/// started/failed pair. The caller has set the emit-once guard + start metadata and deliberately
/// leaves the detector Idle/armed (no terminal latch) so a later fresh install is still detected.
/// </summary>
private void EmitPreinstalled(OfficeC2RSnapshot snap)
{
var data = BuildPayload(snap, "Preinstalled", isTerminal: true);
if (!string.IsNullOrEmpty(_startedTrigger)) data["startedTrigger"] = _startedTrigger!;
// Why we classified it as preinstalled — core binaries already on disk at first signal.
data["reason"] = "office_already_resident";
var product = snap.Products != null && snap.Products.Count > 0 ? string.Join(",", snap.Products) : AppName;
_post.Emit(new EnrollmentEvent
{
SessionId = _sessionId,
TenantId = _tenantId,
EventType = Constants.EventTypes.OfficePreinstalledDetected,
Severity = EventSeverity.Info,
Source = SourceName,
Phase = EnrollmentPhase.Unknown,
Message = $"{SourceName}: {product} already present (pre-installed OEM/consumer Office — no enrollment install)",
Data = data,
ImmediateUpload = true,
});
}
/// <summary>
/// Persists the lifecycle state (fail-soft). Active and the two terminals are persisted;
/// <see cref="AbandonSilently"/> deliberately is NOT — it fires on every dispose/shutdown
@@ -447,6 +546,7 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Telemetry.Office
{ "streamingFinished", snap.StreamingFinished },
{ "officeC2RClientRunning", snap.OfficeC2RClientRunning },
{ "errorCode", (object?)snap.ErrorCode ?? string.Empty },
{ "errorSource", (object?)snap.ErrorSource ?? string.Empty },
{ "installationPath", snap.InstallationPath ?? string.Empty },
{ "scanErrors", (snap.Errors ?? new List<string>()).ToList() },
};
@@ -587,6 +687,17 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Telemetry.Office
foreach (var scenarioName in scenarioNames)
{
// Attribute an error code ONLY from an install-class scenario. A failure value
// in a CLIENTUPDATE / UPDATE / maintenance scenario (the C2R client self-update
// and similar) must never fail an Office *install* — field session fa526757 /
// the CLIENTUPDATE errorCode "2" false positive.
var isInstallScenario = IsInstallScenario(scenarioName);
if (isInstallScenario && snap.InstallScenarioName == null)
{
snap.InstallScenarioPresent = true;
snap.InstallScenarioName = scenarioName;
}
try
{
using (var scenarioKey = scenarioRoot.OpenSubKey(scenarioName, writable: false))
@@ -598,11 +709,12 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Telemetry.Office
if (value == null) continue;
snap.ScenarioValues[$"{scenarioName}\\{valueName}"] = value;
if (snap.ErrorCode == null
if (isInstallScenario && snap.ErrorCode == null
&& ErrorValueHints.Any(h => valueName.IndexOf(h, StringComparison.OrdinalIgnoreCase) >= 0)
&& IsNonZeroNumericCode(value))
{
snap.ErrorCode = value;
snap.ErrorSource = $"{scenarioName}\\{valueName}";
}
}
}
@@ -660,10 +772,18 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Telemetry.Office
return v == "1" || v.Equals("true", StringComparison.OrdinalIgnoreCase);
}
// Below this, a bare positive decimal is treated as an implausible failure code (a state /
// phase / mode / retry-count enum, not an error). Real C2R failures are HRESULTs (hex or
// negative decimal) or 5-digit C2R codes (17002, 30015, 30088…) — all >= this floor. Field
// session fa526757 / the CLIENTUPDATE "2" false positive motivated this.
private const long MinPlausibleDecimalErrorCode = 1000;
/// <summary>
/// True only when <paramref name="value"/> is a non-zero numeric or hex (0x…) code. Textual
/// values such as "Success" / "Completed" / "InProgress" are NOT error codes and return false,
/// so a benign "Result=Success" never masquerades as a failure.
/// True only when <paramref name="value"/> is a PLAUSIBLE non-zero error code: a non-zero hex
/// (0x…) value, a negative decimal HRESULT, or a positive decimal at/above
/// <see cref="MinPlausibleDecimalErrorCode"/>. Textual values ("Success" / "Completed" /
/// "InProgress") and implausibly small positive decimals (e.g. "2") return false, so a benign
/// status/enum value never masquerades as a failure.
/// </summary>
internal static bool IsNonZeroNumericCode(string value)
{
@@ -673,8 +793,10 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Telemetry.Office
{
return long.TryParse(v.Substring(2), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var hex) && hex != 0;
}
// Decimal (HRESULTs can be large or negative); reject anything non-numeric.
return long.TryParse(v, NumberStyles.Integer, CultureInfo.InvariantCulture, out var dec) && dec != 0;
// Decimal: reject non-numeric, zero, and implausibly small positive values; keep negative
// HRESULTs and large positive C2R codes.
if (!long.TryParse(v, NumberStyles.Integer, CultureInfo.InvariantCulture, out var dec) || dec == 0) return false;
return dec < 0 || dec >= MinPlausibleDecimalErrorCode;
}
}
@@ -694,8 +816,14 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Telemetry.Office
public bool StreamingFinished { get; set; }
public bool ActiveScenarioPresent { get; set; }
public string? ActiveScenarioName { get; set; }
/// <summary>True when an install-class scenario (INSTALL) subkey is present.</summary>
public bool InstallScenarioPresent { get; set; }
/// <summary>The install-class scenario subkey name, when present (else null).</summary>
public string? InstallScenarioName { get; set; }
public Dictionary<string, string> ScenarioValues { get; } = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
public string? ErrorCode { get; set; }
/// <summary>Which <c>scenario\value</c> produced <see cref="ErrorCode"/> — for diagnosis.</summary>
public string? ErrorSource { get; set; }
public bool OfficeC2RClientRunning { get; set; }
public List<string> Errors { get; } = new List<string>();
}
@@ -92,8 +92,14 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Telemetry.Office
public const string StateActive = "Active";
public const string StateCompleted = "Completed";
public const string StateFailed = "Failed";
// The C2R activity was on an already-resident Office (OEM/consumer inbox) — a
// office_preinstalled_detected went out, no install lifecycle was opened. Persisted ONLY to
// suppress a duplicate emit across reboots; deliberately NOT terminal (see IsTerminal), because
// an enrollment commonly uninstalls the inbox Office and lays down a fresh one afterwards — the
// next run must re-arm the detector to catch that real install.
public const string StatePreinstalled = "Preinstalled";
/// <summary><see cref="StateActive"/>, <see cref="StateCompleted"/> or <see cref="StateFailed"/>.</summary>
/// <summary><see cref="StateActive"/>, <see cref="StateCompleted"/>, <see cref="StateFailed"/> or <see cref="StatePreinstalled"/>.</summary>
public string? State { get; set; }
public DateTime? StartedAtUtc { get; set; }
@@ -102,6 +108,8 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Telemetry.Office
/// <summary>Highest-bytes DO sample seen — basis for the doSummary on a resumed completion.</summary>
public OfficeDoPeakData? PeakDo { get; set; }
// Preinstalled is intentionally excluded — it is persisted for emit-once dedup but must NOT
// block the next run from arming the detector (a later fresh install must still be detected).
[JsonIgnore]
public bool IsTerminal => State == StateCompleted || State == StateFailed;
}
@@ -0,0 +1,48 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Linq;
namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Telemetry.Office
{
/// <summary>
/// Classifies a C2R <c>ProductReleaseIds</c> set as consumer / OEM-inbox Office vs an IT-managed
/// deployment. Used to label an already-resident Office (core binaries present at the first
/// signal) as preinstalled inbox Office — e.g. the <c>O365HomePremRetail</c> + <c>OneNoteFreeRetail</c>
/// that ship pre-provisioned on many OEM Windows images and run background CLIENTUPDATE scenarios
/// during an enrollment — rather than mis-reporting it as a Microsoft 365 Apps install/failure.
/// </summary>
internal static class OfficeProductClassifier
{
// Enterprise / IT-managed C2R product markers (Microsoft 365 Apps for enterprise/business,
// volume). When ANY product carries one of these the set is treated as managed, never
// "consumer inbox" — even if a consumer SKU is co-listed. NOTE: the business marker is the
// specific "O365Business" prefix (the managed "Microsoft 365 Apps for business" id, e.g.
// O365BusinessRetail), NOT a bare "Business" — otherwise the consumer perpetual HomeBusiness*Retail
// SKUs would be wrongly caught here before the consumer markers below.
private static readonly string[] ManagedMarkers = { "ProPlus", "O365Business", "Volume" };
// Consumer / OEM-preinstall SKU markers (substring, case-insensitive).
private static readonly string[] ConsumerMarkers =
{
"HomePrem", "HomeStudent", "HomeBusiness", "Personal", "OneNoteFree", "Professional",
};
/// <summary>
/// True when the product set is non-empty AND every entry looks like a consumer / OEM-inbox SKU
/// and none is an enterprise/business/volume SKU. Conservative: an unknown SKU (matching neither
/// list) makes the set NOT consumer, so an unrecognized managed product is never mislabeled.
/// </summary>
internal static bool IsConsumerInboxProductSet(IReadOnlyList<string>? products)
{
if (products == null || products.Count == 0) return false;
foreach (var p in products)
{
if (string.IsNullOrWhiteSpace(p)) return false;
if (ManagedMarkers.Any(m => p.IndexOf(m, StringComparison.OrdinalIgnoreCase) >= 0)) return false;
if (!ConsumerMarkers.Any(m => p.IndexOf(m, StringComparison.OrdinalIgnoreCase) >= 0)) return false;
}
return true;
}
}
}
@@ -298,9 +298,12 @@ namespace AutopilotMonitor.Agent.V2.Core.Orchestration
// several reboots, and Scenario\INSTALL persists post-install / OfficeC2RClient.exe
// runs again for update checks / Office updates stream from the same CDN — all three
// start triggers would falsely re-open the window and emit a duplicate
// started+completed pair every restart). A persisted terminal means the install was
// already reported → don't arm the detector at all this run; a persisted Active is
// resumed inside the host (no second started, missed completion delivered late).
// started+completed pair every restart). A persisted terminal (Completed/Failed) means
// the install was already reported → don't arm the detector at all this run; a persisted
// Active is resumed inside the host (no second started, missed completion delivered late).
// A persisted Preinstalled is NOT terminal — the host is still armed (an enrollment often
// uninstalls the inbox Office and installs a fresh one), it only suppresses a duplicate
// office_preinstalled_detected via ResumePreinstalled.
var officeStatePersistence = new OfficeInstallStatePersistence(_stateDirectory, logger);
var officeState = officeStatePersistence.Load();
if (officeState != null && officeState.IsTerminal)
@@ -33,6 +33,12 @@ namespace AutopilotMonitor.Agent.V2.Core.Orchestration
/// An explicit error code → failed. No binary ever appears → the lifecycle simply stays open and is
/// abandoned silently on dispose (never a false completed/failed).
/// </para>
/// <para>
/// <b>Already-resident short-circuit:</b> when the core Office binaries are already on disk at the
/// first signal, the detector does not open an install lifecycle — a consumer/OEM-inbox product set
/// emits an informational <c>office_preinstalled_detected</c> (no binary watcher armed), enterprise
/// Office falls through to the synchronous started→completed path. See <see cref="OfficeInstallDetector"/>.
/// </para>
/// </summary>
internal sealed class OfficeInstallDetectorHost : ICollectorHost
{
@@ -91,6 +97,14 @@ namespace AutopilotMonitor.Agent.V2.Core.Orchestration
public void Start()
{
// Restore the preinstalled emit-once guard BEFORE arming any watcher. The process watcher's
// startup probe can synchronously fire Started (→ OnWorkerStarted → BeginIfIdle) when
// OfficeC2RClient.exe is already running, which would otherwise re-emit
// office_preinstalled_detected for the resident inbox Office. ResumePreinstalled only sets
// the guard (no watcher/InstallationPath interaction), so it is safe to run this early.
if (_resumeState != null && _resumeState.State == OfficeInstallStateData.StatePreinstalled)
_detector.ResumePreinstalled(_resumeState);
// Arm the registry watcher up front (not at worker-start) so the early Scenario\INSTALL key
// is caught before the (late) worker process. The watcher's bootstrap fallback handles the
// case where the ClickToRun key does not exist yet on a clean first install.
@@ -102,11 +116,12 @@ namespace AutopilotMonitor.Agent.V2.Core.Orchestration
}
_processWatcher.Start();
// Resume a lifecycle left open by a previous agent run (enrollments span reboots) AFTER
// the watchers are armed: the resume surfaces the InstallationPath, which arms the binary
// watcher — when Office finished installing while the agent was down, its initial scan
// completes the lifecycle synchronously and the teardown disposes the watchers again.
if (_resumeState != null) _detector.ResumeActive(_resumeState);
// Resume a persisted *Active* lifecycle (enrollments span reboots) AFTER the watchers are
// armed: the resume surfaces the InstallationPath, which arms the binary watcher — when
// Office finished installing while the agent was down, its initial scan completes the
// lifecycle synchronously and the teardown disposes the just-armed watchers again.
if (_resumeState != null && _resumeState.State != OfficeInstallStateData.StatePreinstalled)
_detector.ResumeActive(_resumeState);
}
private void OnWorkerStarted(object? sender, EventArgs e) => _detector.OnWorkerStarted();
@@ -69,6 +69,7 @@ export const EVENT_TYPES_CATALOG = {
'office_install_progress',
'office_install_completed',
'office_install_failed',
'office_preinstalled_detected',
],
script_events: ['script_started', 'script_completed', 'script_failed'],
network_events: [
@@ -324,6 +324,10 @@ namespace AutopilotMonitor.Shared
public const string OfficeInstallProgress = "office_install_progress";
public const string OfficeInstallCompleted = "office_install_completed";
public const string OfficeInstallFailed = "office_install_failed";
// Emitted instead of a started/failed pair when the C2R activity is on an Office that is
// ALREADY fully resident on disk at the first signal (OEM/consumer inbox Office running a
// background CLIENTUPDATE/maintenance scenario). Informational — NOT an enrollment failure.
public const string OfficePreinstalledDetected = "office_preinstalled_detected";
// Stall detection (Ebene 2 — StallProbeCollector)
public const string StallProbeCheck = "stall_probe_check"; // Trace heartbeat from Probe 2 (15 min) when no anomaly found
@@ -96,6 +96,8 @@ export const KNOWN_EVENT_TYPES: EventTypeEntry[] = [
description: "Office C2R install completed (streaming finished, scenario cleared); carries version reached and duration." },
{ value: "office_install_failed", label: "office_install_failed", category: "app",
description: "Office C2R install ended without reaching completion, or a failure code was observed." },
{ value: "office_preinstalled_detected", label: "office_preinstalled_detected", category: "app",
description: "Office was already fully resident on disk at the first signal (OEM/consumer inbox Office running a background CLIENTUPDATE) — informational, not an enrollment install or failure." },
// -------- Scripts --------
{ value: "script_started", label: "script_started", category: "script",
@@ -600,7 +600,7 @@ export default function SessionDetailPage() {
<InstallProgress
events={events.filter(
e => e.eventType === "app_install_started" || e.eventType === "app_install_completed" || e.eventType === "app_install_failed" || e.eventType === "app_install_postponed" || e.eventType === "app_install_skipped"
|| e.eventType === "office_install_started" || e.eventType === "office_install_completed" || e.eventType === "office_install_failed"
|| e.eventType === "office_install_started" || e.eventType === "office_install_completed" || e.eventType === "office_install_failed" || e.eventType === "office_preinstalled_detected"
|| e.eventType === "realmjoin_package_started" || e.eventType === "realmjoin_package_completed"
)}
summaryStats={appSummaryStats}
@@ -32,7 +32,7 @@ const ESP_APPS_INSTALL_FAILURE = "esp_apps_install_failure";
interface InstallItem {
appName: string;
appId: string;
state: "Installing" | "Installed" | "Failed" | "Postponed" | "Skipped";
state: "Installing" | "Installed" | "Failed" | "Postponed" | "Skipped" | "Preinstalled";
startedAt?: string;
completedAt?: string;
durationMs?: number;
@@ -206,6 +206,24 @@ export default function InstallProgress({ events, summaryStats }: InstallProgres
firstSeenIndex: existing?.firstSeenIndex ?? insertionIndex++,
eventData: d,
});
} else if (type === "office_preinstalled_detected") {
// Office was already fully resident on disk at the first signal (OEM/consumer inbox Office
// running a background CLIENTUPDATE) — informational, not an enrollment install or failure.
// Don't overwrite a real terminal state if one somehow co-exists.
if (existing?.state === "Installed" || existing?.state === "Failed") continue;
installMap.set(appName, {
appName,
appId,
state: "Preinstalled",
completedAt: eventTs,
isCompleted: true,
isError: false,
isLikelyStuck: false,
isDetectionFailure: false,
isInstallFailure: false,
firstSeenIndex: existing?.firstSeenIndex ?? insertionIndex++,
eventData: d,
});
} else if (evt.eventType === "app_install_postponed") {
// Don't downgrade from Installed to Postponed.
if (existing?.state === "Installed") continue;
@@ -384,13 +402,15 @@ function InstallItemRow({ item }: { item: InstallItem }) {
// have a concrete HRESULT and are confirmed errors → red, same as any other.
const containerClass = item.state === "Skipped"
? "bg-gray-50 border border-gray-300"
: item.isLikelyStuck
? "bg-orange-50 border border-orange-200"
: item.isError
? "bg-red-50 border border-red-200"
: item.isCompleted
? "bg-green-50 border border-green-200"
: "bg-gray-50 border border-gray-200";
: item.state === "Preinstalled"
? "bg-sky-50 border border-sky-200"
: item.isLikelyStuck
? "bg-orange-50 border border-orange-200"
: item.isError
? "bg-red-50 border border-red-200"
: item.isCompleted
? "bg-green-50 border border-green-200"
: "bg-gray-50 border border-gray-200";
return (
<div className={`rounded-lg p-3 ${containerClass}`}>
@@ -400,6 +420,11 @@ function InstallItemRow({ item }: { item: InstallItem }) {
<svg className="w-4 h-4 text-gray-400 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 5l7 7-7 7M5 5l7 7-7 7" />
</svg>
) : item.state === "Preinstalled" ? (
// Info circle — already-resident Office, neither install nor failure.
<svg className="w-4 h-4 text-sky-500 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
) : item.isLikelyStuck ? (
// Question-mark inside circle — explicit "we don't actually know" iconography,
// distinct from the hard X used for confirmed failures.
@@ -455,6 +480,14 @@ function InstallItemRow({ item }: { item: InstallItem }) {
{item.state === "Postponed" && (
<span className="text-xs px-2 py-0.5 rounded-full bg-amber-200 text-amber-700 font-medium">Postponed</span>
)}
{item.state === "Preinstalled" && (
<span
className="text-xs px-2 py-0.5 rounded-full bg-sky-200 text-sky-800 font-medium"
title="Office was already present on disk at enrollment start (OEM/consumer inbox Office) — not installed by this enrollment"
>
Pre-installed
</span>
)}
</div>
<div className="flex items-center space-x-3 text-xs text-gray-500 flex-shrink-0 ml-2">
{elapsedMs != null && elapsedMs > 0 && (