diff --git a/src/Agent/AutopilotMonitor.Agent.V2.Core.Tests/Monitoring/Runtime/BootstrapProvenanceReaderTests.cs b/src/Agent/AutopilotMonitor.Agent.V2.Core.Tests/Monitoring/Runtime/BootstrapProvenanceReaderTests.cs
index 012b8418..909893e8 100644
--- a/src/Agent/AutopilotMonitor.Agent.V2.Core.Tests/Monitoring/Runtime/BootstrapProvenanceReaderTests.cs
+++ b/src/Agent/AutopilotMonitor.Agent.V2.Core.Tests/Monitoring/Runtime/BootstrapProvenanceReaderTests.cs
@@ -12,7 +12,8 @@ namespace AutopilotMonitor.Agent.V2.Core.Tests.Monitoring.Runtime
/// The bootstrap provenance in agent_started. Fixture lines are the scripts' real
/// formats: installer [ts] Bootstrap script version: vX.Y
/// (Install-AutopilotMonitor.ps1), loader [ts] === Autopilot Monitor bootstrap loader vX.Y
- /// started (user=..., pid=...) === (Start-AutopilotMonitor.ps1).
+ /// started (user=..., pid=...) === (Start-AutopilotMonitor.ps1), the MSI's downloader before
+ /// the loader [ts] MSI bootstrap runner started (user=..., pid=...)..
///
public sealed class BootstrapProvenanceReaderTests : IDisposable
{
@@ -79,6 +80,39 @@ namespace AutopilotMonitor.Agent.V2.Core.Tests.Monitoring.Runtime
Assert.Null(p.ScriptVersion);
}
+ [Fact]
+ public void An_installer_from_before_the_log_rename_is_read_under_its_old_name()
+ {
+ Write(BootstrapProvenanceReader.LegacyInstallerLogName, InstallerRun("2.3"));
+
+ var p = BootstrapProvenanceReader.Read(_dir);
+
+ Assert.True(p.ScriptLogPresent);
+ Assert.Equal("2.3", p.ScriptVersion);
+ }
+
+ [Fact]
+ public void With_both_installer_logs_the_most_recently_written_one_counts()
+ {
+ Write(BootstrapProvenanceReader.InstallerLogName, InstallerRun("2.6"));
+ Write(BootstrapProvenanceReader.LegacyInstallerLogName, InstallerRun("2.3"));
+ File.SetLastWriteTimeUtc(Path.Combine(_dir, BootstrapProvenanceReader.InstallerLogName), DateTime.UtcNow.AddDays(-3));
+ File.SetLastWriteTimeUtc(Path.Combine(_dir, BootstrapProvenanceReader.LegacyInstallerLogName), DateTime.UtcNow.AddMinutes(-1));
+
+ Assert.Equal("2.3", BootstrapProvenanceReader.Read(_dir).ScriptVersion);
+ }
+
+ [Fact]
+ public void The_msi_downloader_from_before_the_loader_counts_as_msi_without_version()
+ {
+ Write(BootstrapProvenanceReader.LegacyMsiLoaderLogName, "[2026-08-01 09:00:00] MSI bootstrap runner started (user=SECRETUSER, pid=4242).");
+
+ var p = BootstrapProvenanceReader.Read(_dir);
+
+ Assert.Equal("msi", p.LoaderLog);
+ Assert.Null(p.LoaderVersion);
+ }
+
[Fact]
public void The_msi_loader_log_is_told_apart_from_the_intune_loader()
{
diff --git a/src/Agent/AutopilotMonitor.Agent.V2.Core/Monitoring/Runtime/BootstrapProvenanceReader.cs b/src/Agent/AutopilotMonitor.Agent.V2.Core/Monitoring/Runtime/BootstrapProvenanceReader.cs
index 94060c07..86e1f6d4 100644
--- a/src/Agent/AutopilotMonitor.Agent.V2.Core/Monitoring/Runtime/BootstrapProvenanceReader.cs
+++ b/src/Agent/AutopilotMonitor.Agent.V2.Core/Monitoring/Runtime/BootstrapProvenanceReader.cs
@@ -13,10 +13,13 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Runtime
/// passing a version to --install would only ever cover installers released after that
/// change -- the old installers still assigned in Intune are exactly the ones worth counting.
///
- /// - bootstrap-script.log: Bootstrap script version: vX.Y, written by every
+ ///
- bootstrap-script.log (bootstrap_agent.log up to installer v2.3, before
+ /// the log rename of 2026-08-18): Bootstrap script version: vX.Y, written by every
/// installer run since 2026-04-09. A log without that line means an older installer.
- /// - bootstrap-loader.log (Intune) / bootstrap-msi.log (MSI):
- /// bootstrap loader vX.Y started, written by every loader since v1.0.
+ /// - bootstrap-loader.log (Intune) / bootstrap-msi.log (MSI,
+ /// msi-bootstrap.log before 2026-08-18): bootstrap loader vX.Y started, written by
+ /// every loader since v1.0. The MSI's own downloader, which the loader replaced on 2026-09-08,
+ /// writes no version.
/// - oobe_bootstrap.log: the OOBE go-script (generated by the backend, no version).
///
/// Only the version number is extracted; the loader line also names the account, which is
@@ -29,6 +32,10 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Runtime
internal const string MsiLoaderLogName = "bootstrap-msi.log";
internal const string OobeLogName = "oobe_bootstrap.log";
+ // Names before the log rename of 2026-08-18; copies of those scripts are still assigned.
+ internal const string LegacyInstallerLogName = "bootstrap_agent.log";
+ internal const string LegacyMsiLoaderLogName = "msi-bootstrap.log";
+
// The logs grow across runs; the last run's lines sit at the end. One run writes a few KB.
private const int TailBytes = 64 * 1024;
@@ -42,22 +49,19 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Runtime
{
try
{
- var installerLog = ReadTail(Path.Combine(logDirectory, InstallerLogName));
+ var installerLog = ReadTail(MostRecentlyWritten(logDirectory, InstallerLogName, LegacyInstallerLogName));
- // Intune loader and MSI loader write separate files; when a device has both, the
- // more recently written one belongs to the bootstrap that ran last.
- var loader = new[] { (Name: LoaderLogName, Channel: "loader"), (Name: MsiLoaderLogName, Channel: "msi") }
- .Select(l => (l.Channel, Path: Path.Combine(logDirectory, l.Name)))
- .Where(l => File.Exists(l.Path))
- .OrderByDescending(l => File.GetLastWriteTimeUtc(l.Path))
- .Select(l => (l.Channel, Version: LastVersion(ReadTail(l.Path), LoaderStartLine)))
- .FirstOrDefault();
+ // Intune loader and MSI loader write separate files; every name but the Intune
+ // loader's is the MSI's.
+ var loaderPath = MostRecentlyWritten(logDirectory, LoaderLogName, MsiLoaderLogName, LegacyMsiLoaderLogName);
+ var loaderChannel = loaderPath == null ? null
+ : string.Equals(Path.GetFileName(loaderPath), LoaderLogName, StringComparison.OrdinalIgnoreCase) ? "loader" : "msi";
return new BootstrapProvenance(
scriptLogPresent: installerLog != null,
scriptVersion: LastVersion(installerLog, InstallerVersionLine),
- loaderLog: loader.Channel,
- loaderVersion: loader.Version,
+ loaderLog: loaderChannel,
+ loaderVersion: LastVersion(ReadTail(loaderPath), LoaderStartLine),
oobeLogPresent: File.Exists(Path.Combine(logDirectory, OobeLogName)));
}
catch
@@ -66,6 +70,16 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Runtime
}
}
+ ///
+ /// The most recently written of the named logs that exist, or null. On a device with
+ /// several, it belongs to the bootstrap that ran last.
+ ///
+ private static string MostRecentlyWritten(string directory, params string[] names) =>
+ names.Select(name => Path.Combine(directory, name))
+ .Where(path => File.Exists(path))
+ .OrderByDescending(path => File.GetLastWriteTimeUtc(path))
+ .FirstOrDefault();
+
internal static string LastVersion(string text, Regex line)
{
if (string.IsNullOrEmpty(text)) return null;