diff --git a/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Core/MonitoringService.CollectorLifecycle.cs b/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Core/MonitoringService.CollectorLifecycle.cs
index 52a97b43..50437f3e 100644
--- a/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Core/MonitoringService.CollectorLifecycle.cs
+++ b/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Core/MonitoringService.CollectorLifecycle.cs
@@ -589,17 +589,8 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Core
// Run shutdown analyzers before final event (captures end-state for delta detection)
RunShutdownAnalyzers();
- // Emit final event
- EmitEvent(new EnrollmentEvent
- {
- SessionId = _configuration.SessionId,
- TenantId = _configuration.TenantId,
- EventType = "agent_stopped",
- Severity = EventSeverity.Info,
- Source = "Agent",
- Phase = _lastPhase ?? EnrollmentPhase.Unknown,
- Message = "Autopilot Monitor Agent stopped"
- });
+ // Emit agent_shutdown as counterpart to agent_started
+ EmitShutdownEvent("manual_stop", "Autopilot Monitor Agent stopped");
// Final upload attempt
UploadEventsAsync().Wait(TimeSpan.FromSeconds(10));
diff --git a/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Core/MonitoringService.EventUpload.cs b/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Core/MonitoringService.EventUpload.cs
index a81b11f9..466f9680 100644
--- a/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Core/MonitoringService.EventUpload.cs
+++ b/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Core/MonitoringService.EventUpload.cs
@@ -191,6 +191,38 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Core
});
}
+ ///
+ /// Emits an agent_shutdown event as the counterpart to agent_started.
+ /// Called at every controlled exit point so the timeline shows why and how the agent terminated.
+ ///
+ private void EmitShutdownEvent(string reason, string message, Dictionary extraData = null)
+ {
+ var data = new Dictionary
+ {
+ { "reason", reason },
+ { "agentVersion", _agentVersion },
+ { "uptimeMinutes", Math.Round((DateTime.UtcNow - _agentStartTimeUtc).TotalMinutes, 1) }
+ };
+ if (extraData != null)
+ {
+ foreach (var kvp in extraData)
+ data[kvp.Key] = kvp.Value;
+ }
+
+ EmitEvent(new EnrollmentEvent
+ {
+ SessionId = _configuration.SessionId,
+ TenantId = _configuration.TenantId,
+ Timestamp = DateTime.UtcNow,
+ EventType = "agent_shutdown",
+ Severity = EventSeverity.Info,
+ Source = "Agent",
+ Phase = _lastPhase ?? EnrollmentPhase.Unknown,
+ Message = message,
+ Data = data
+ });
+ }
+
///
/// Called when new events are available in the spool (FileSystemWatcher detected new files)
/// Uses debouncing to batch events before uploading
@@ -264,6 +296,34 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Core
_uploadTimer?.Change(Timeout.Infinite, Timeout.Infinite);
_debounceTimer?.Change(Timeout.Infinite, Timeout.Infinite);
+ // Emit agent_shutdown event and try to upload it directly (we hold the upload
+ // semaphore so a normal UploadEventsAsync would skip — send inline instead)
+ EmitShutdownEvent(
+ "remote_kill_signal",
+ "Agent terminated by remote kill signal from administrator",
+ new Dictionary
+ {
+ { "pendingEvents", _spool.GetCount() },
+ { "selfDestruct", true }
+ });
+ try
+ {
+ var shutdownEvents = _spool.GetBatch(_configuration.MaxBatchSize);
+ if (shutdownEvents.Count > 0)
+ {
+ await _apiClient.IngestEventsAsync(new IngestEventsRequest
+ {
+ SessionId = _configuration.SessionId,
+ TenantId = _configuration.TenantId,
+ Events = shutdownEvents
+ });
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger.Warning($"Failed to upload shutdown event before kill: {ex.Message}");
+ }
+
// Force self-destruct regardless of current config, respecting KeepLogFile from tenant config
_configuration.SelfDestructOnComplete = true;
_cleanupService.ExecuteSelfDestruct();
@@ -376,6 +436,17 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Core
_logger.Error($"=== AGENT SHUTDOWN: {_consecutiveAuthFailures} consecutive authentication failures (401/403). " +
"The device is not authorized to send data to Autopilot Monitor. " +
"Check client certificate and Autopilot device validation in your tenant configuration. ===");
+
+ // Emit shutdown event (stays in spool for diagnostics — upload not possible due to auth failure)
+ EmitShutdownEvent(
+ "auth_failure",
+ $"Agent terminated: {_consecutiveAuthFailures} consecutive authentication failures exceeded threshold ({_configuration.MaxAuthFailures})",
+ new Dictionary
+ {
+ { "consecutiveFailures", _consecutiveAuthFailures },
+ { "shutdownTrigger", "max_attempts" },
+ { "maxAuthFailures", _configuration.MaxAuthFailures }
+ });
Environment.Exit(1);
}
@@ -389,6 +460,18 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Core
$"(timeout: {_configuration.AuthFailureTimeoutMinutes} min). " +
"The device is not authorized to send data to Autopilot Monitor. " +
"Check client certificate and Autopilot device validation in your tenant configuration. ===");
+
+ // Emit shutdown event (stays in spool for diagnostics — upload not possible due to auth failure)
+ EmitShutdownEvent(
+ "auth_failure",
+ $"Agent terminated: authentication failures persisted for {elapsed.TotalMinutes:F0} minutes (timeout: {_configuration.AuthFailureTimeoutMinutes} min)",
+ new Dictionary
+ {
+ { "consecutiveFailures", _consecutiveAuthFailures },
+ { "shutdownTrigger", "timeout" },
+ { "authFailureTimeoutMinutes", _configuration.AuthFailureTimeoutMinutes },
+ { "elapsedMinutes", Math.Round(elapsed.TotalMinutes, 1) }
+ });
Environment.Exit(1);
}
}
@@ -547,6 +630,18 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Core
Process.Start(psi);
}
+ // Emit agent_shutdown as counterpart to agent_started
+ EmitShutdownEvent(
+ enrollmentSucceeded ? "enrollment_complete" : "enrollment_failed",
+ $"Agent shutting down after enrollment {(enrollmentSucceeded ? "completion" : "failure")}",
+ new Dictionary
+ {
+ { "enrollmentSucceeded", enrollmentSucceeded },
+ { "selfDestruct", _configuration.SelfDestructOnComplete },
+ { "reboot", _configuration.RebootOnComplete }
+ });
+ await UploadEventsAsync();
+
_logger.Info("Shutdown sequence complete. Agent will now exit.");
// Exit the application
@@ -603,6 +698,20 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Core
// No DeleteSessionId, no WriteEnrollmentCompleteMarker, no self-destruct.
// The Windows Autopilot shutdown will power off the device;
// the Scheduled Task restarts the agent on the user's first boot.
+ // Emit agent_shutdown as counterpart to agent_started
+ EmitShutdownEvent(
+ "whiteglove_complete",
+ "Agent shutting down after WhiteGlove Part 1 — no cleanup, session preserved for Part 2 on next boot",
+ new Dictionary
+ {
+ { "sessionPreserved", true },
+ { "selfDestruct", false },
+ { "reboot", false },
+ { "cleanup", false },
+ { "willResumeOnNextBoot", true }
+ });
+ await UploadEventsAsync();
+
_logger.Info("WhiteGlove graceful shutdown complete. Agent exiting.");
Environment.Exit(0);
}
diff --git a/src/Agent/AutopilotMonitor.Agent/buildcounter.txt b/src/Agent/AutopilotMonitor.Agent/buildcounter.txt
index 1448fd85..629f34ce 100644
--- a/src/Agent/AutopilotMonitor.Agent/buildcounter.txt
+++ b/src/Agent/AutopilotMonitor.Agent/buildcounter.txt
@@ -1 +1 @@
-483
+485