diff --git a/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Collectors/DesktopArrivalDetector.cs b/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Collectors/DesktopArrivalDetector.cs
index c8152a3a..1c286256 100644
--- a/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Collectors/DesktopArrivalDetector.cs
+++ b/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Collectors/DesktopArrivalDetector.cs
@@ -1,4 +1,5 @@
using System;
+using System.Collections.Generic;
using System.Diagnostics;
using System.Management;
using System.Threading;
@@ -17,6 +18,7 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Collectors
private readonly AgentLogger _logger;
private Timer _pollingTimer;
private bool _desktopArrived;
+ private bool _excludedUserTraced; // Emit trace event only once for excluded-user skips
private const int PollingIntervalSeconds = 30;
///
@@ -38,6 +40,12 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Collectors
///
public event EventHandler DesktopArrived;
+ ///
+ /// Optional callback for trace events (decision, reason, context).
+ /// Wired by MonitoringService to emit agent_trace events to the backend.
+ ///
+ public Action> OnTraceEvent { get; set; }
+
public DesktopArrivalDetector(AgentLogger logger)
{
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
@@ -83,6 +91,20 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Collectors
if (IsExcludedUser(owner))
{
_logger.Debug($"DesktopArrivalDetector: explorer.exe PID {proc.Id} owned by excluded user '{owner}' — skipping");
+
+ // Trace this decision once so it's visible in the backend
+ if (!_excludedUserTraced)
+ {
+ _excludedUserTraced = true;
+ try
+ {
+ OnTraceEvent?.Invoke(
+ "desktop_excluded_user",
+ $"explorer.exe found but owned by excluded user '{owner}' — not a real user desktop",
+ new Dictionary { { "pid", proc.Id }, { "session", proc.SessionId }, { "owner", owner } });
+ }
+ catch { }
+ }
continue;
}
@@ -90,6 +112,15 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Collectors
_desktopArrived = true;
_logger.Info($"DesktopArrivalDetector: real user desktop detected (explorer.exe PID {proc.Id}, session {proc.SessionId}, user '{owner}')");
+ try
+ {
+ OnTraceEvent?.Invoke(
+ "desktop_real_user_detected",
+ $"Real user desktop detected (explorer.exe PID {proc.Id}, user '{owner}')",
+ new Dictionary { { "pid", proc.Id }, { "session", proc.SessionId }, { "owner", owner } });
+ }
+ catch { }
+
// Stop polling
_pollingTimer?.Dispose();
_pollingTimer = null;
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 046408a9..645090f4 100644
--- a/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Core/MonitoringService.CollectorLifecycle.cs
+++ b/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Core/MonitoringService.CollectorLifecycle.cs
@@ -106,6 +106,8 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Core
// Desktop arrival detector — detects real user desktop for no-ESP completion
_desktopArrivalDetector = new DesktopArrivalDetector(_logger);
_desktopArrivalDetector.DesktopArrived += OnDesktopArrived;
+ _desktopArrivalDetector.OnTraceEvent = (decision, reason, context) =>
+ EmitTraceEvent("DesktopArrivalDetector", decision, reason, context);
_desktopArrivalDetector.Start();
// Agent max lifetime safety net — prevents zombie agents
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 867c3cb0..5b93e57f 100644
--- a/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Core/MonitoringService.EventUpload.cs
+++ b/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Core/MonitoringService.EventUpload.cs
@@ -129,20 +129,10 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Core
}
});
- // Trigger self-destruct or reboot if configured
- if (_configuration.SelfDestructOnComplete || _configuration.RebootOnComplete)
- {
- Task.Run(() => HandleEnrollmentComplete(enrollmentSucceeded));
- return; // Don't continue with normal event processing
- }
-
- // No self-destruct/reboot — still upload diagnostics and optionally show summary
- Task.Run(async () =>
- {
- await UploadDiagnosticsWithTimelineEvents(enrollmentSucceeded);
- if (_configuration.ShowEnrollmentSummary)
- LaunchEnrollmentSummaryDialog();
- });
+ // Always run the full completion sequence (analyzers, diagnostics, summary, exit).
+ // HandleEnrollmentComplete conditionally self-destructs or reboots based on config.
+ Task.Run(() => HandleEnrollmentComplete(enrollmentSucceeded));
+ return; // Don't continue with normal event processing
}
// Immediate upload for:
@@ -162,6 +152,41 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Core
}
}
+ ///
+ /// Emits a Trace-severity event from any component for agent decision auditing.
+ /// Always logged locally; only sent to the backend when SendTraceEvents is enabled.
+ ///
+ private void EmitTraceEvent(string source, string decision, string reason, Dictionary context = null)
+ {
+ _logger.Info($"[TRACE] {source}: {decision} — {reason}");
+
+ if (!_configuration.SendTraceEvents)
+ return;
+
+ var data = new Dictionary
+ {
+ { "decision", decision },
+ { "reason", reason }
+ };
+ if (context != null)
+ {
+ foreach (var kvp in context)
+ data[kvp.Key] = kvp.Value;
+ }
+
+ EmitEvent(new EnrollmentEvent
+ {
+ SessionId = _configuration.SessionId,
+ TenantId = _configuration.TenantId,
+ EventType = "agent_trace",
+ Severity = EventSeverity.Trace,
+ Source = source,
+ Phase = EnrollmentPhase.Unknown,
+ Message = $"{decision}: {reason}",
+ Data = data
+ });
+ }
+
///
/// Called when new events are available in the spool (FileSystemWatcher detected new files)
/// Uses debouncing to batch events before uploading
@@ -424,7 +449,7 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Core
{
try
{
- _logger.Info("===== ENROLLMENT COMPLETE - Starting Self-Destruct Sequence =====");
+ _logger.Info("===== ENROLLMENT COMPLETE - Starting shutdown sequence =====");
// Step 1: Stop all event collectors
_logger.Info("Stopping event collectors...");
@@ -487,7 +512,7 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Core
Process.Start(psi);
}
- _logger.Info("Self-destruct sequence initiated. Agent will now exit.");
+ _logger.Info("Shutdown sequence complete. Agent will now exit.");
// Exit the application
Environment.Exit(0);
@@ -583,9 +608,13 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Core
return;
}
- // Create temp folder for the dialog (survives agent self-destruct)
- var tempDir = Path.Combine(Path.GetTempPath(),
- "AutopilotMonitor-Summary-" + Guid.NewGuid().ToString("N").Substring(0, 8));
+ // Create temp folder for the dialog (survives agent self-destruct).
+ // Use ProgramData instead of Path.GetTempPath() because the agent runs as SYSTEM
+ // and SYSTEM's temp (C:\WINDOWS\SystemTemp) is not accessible to the user session.
+ var tempDir = Path.Combine(
+ Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData),
+ "AutopilotMonitor",
+ "Summary-" + Guid.NewGuid().ToString("N").Substring(0, 8));
Directory.CreateDirectory(tempDir);
// Copy dialog EXE + Newtonsoft.Json.dll + final-status.json to temp
diff --git a/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Tracking/EnrollmentTracker.EventHandlers.cs b/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Tracking/EnrollmentTracker.EventHandlers.cs
index 78db9cbe..44c4a4c9 100644
--- a/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Tracking/EnrollmentTracker.EventHandlers.cs
+++ b/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Tracking/EnrollmentTracker.EventHandlers.cs
@@ -432,12 +432,40 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Tracking
_logger.Info($"EnrollmentTracker: {message}");
}
+ private void HandleImeSessionChange(string changeType)
+ {
+ _emitEvent(new EnrollmentEvent
+ {
+ SessionId = _sessionId,
+ TenantId = _tenantId,
+ EventType = "ime_session_change",
+ Severity = EventSeverity.Debug,
+ Source = "ImeLogTracker",
+ Phase = EnrollmentPhase.Unknown,
+ Message = $"IME session change: {changeType}",
+ Data = new Dictionary { { "changeType", changeType ?? "" } }
+ });
+ }
+
private void HandleUserSessionCompleted()
{
_logger.Info("EnrollmentTracker: User session completed (detected from IME log)");
_stateData.ImePatternSeenUtc = DateTime.UtcNow;
RecordSignal("ime_pattern");
+ // Emit timeline event so admins can see exactly when IME finished its user part
+ EmitImeTrackerEvent(new EnrollmentEvent
+ {
+ SessionId = _sessionId,
+ TenantId = _tenantId,
+ EventType = "ime_user_session_completed",
+ Severity = EventSeverity.Info,
+ Source = "ImeLogTracker",
+ Phase = EnrollmentPhase.Unknown,
+ Message = "IME user session completed",
+ Data = new Dictionary { { "detectedAt", DateTime.UtcNow.ToString("o") } }
+ });
+
// User session completed successfully — cancel any pending ESP failure
CancelPendingEspFailure();
diff --git a/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Tracking/EnrollmentTracker.cs b/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Tracking/EnrollmentTracker.cs
index 67ec7910..e184dd76 100644
--- a/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Tracking/EnrollmentTracker.cs
+++ b/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Tracking/EnrollmentTracker.cs
@@ -179,6 +179,7 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Tracking
_imeLogTracker.OnPoliciesDiscovered = HandlePoliciesDiscovered;
_imeLogTracker.OnAllAppsCompleted = HandleAllAppsCompleted;
_imeLogTracker.OnUserSessionCompleted = HandleUserSessionCompleted;
+ _imeLogTracker.OnImeSessionChange = HandleImeSessionChange;
_imeLogTracker.OnDoTelemetryReceived = HandleDoTelemetryReceived;
_imeLogTracker.OnScriptCompleted = HandleScriptCompleted;
diff --git a/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Tracking/ImeLogTracker.LogProcessing.cs b/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Tracking/ImeLogTracker.LogProcessing.cs
index f14ac336..dec47f23 100644
--- a/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Tracking/ImeLogTracker.LogProcessing.cs
+++ b/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Tracking/ImeLogTracker.LogProcessing.cs
@@ -138,7 +138,9 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Tracking
break;
case "imesessionchange":
- _logger.Debug($"IME session change: {match.Groups["change"]?.Value}");
+ var sessionChange = match.Groups["change"]?.Value;
+ _logger.Debug($"IME session change: {sessionChange}");
+ OnImeSessionChange?.Invoke(sessionChange);
break;
case "espphasedetected":
diff --git a/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Tracking/ImeLogTracker.cs b/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Tracking/ImeLogTracker.cs
index f67d7559..a038f38e 100644
--- a/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Tracking/ImeLogTracker.cs
+++ b/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Tracking/ImeLogTracker.cs
@@ -112,6 +112,7 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Tracking
public Action OnPoliciesDiscovered { get; set; }
public Action OnAllAppsCompleted { get; set; }
public Action OnUserSessionCompleted { get; set; }
+ public Action OnImeSessionChange { get; set; }
public Action OnDoTelemetryReceived { get; set; }
public Action OnScriptCompleted { get; set; }
diff --git a/src/Agent/AutopilotMonitor.Agent/buildcounter.txt b/src/Agent/AutopilotMonitor.Agent/buildcounter.txt
index 336dd5e3..f20bd4ef 100644
--- a/src/Agent/AutopilotMonitor.Agent/buildcounter.txt
+++ b/src/Agent/AutopilotMonitor.Agent/buildcounter.txt
@@ -1 +1 @@
-289
+292
diff --git a/src/Agent/AutopilotMonitor.SummaryDialog/App.xaml.cs b/src/Agent/AutopilotMonitor.SummaryDialog/App.xaml.cs
index eb13dc2c..73edefa3 100644
--- a/src/Agent/AutopilotMonitor.SummaryDialog/App.xaml.cs
+++ b/src/Agent/AutopilotMonitor.SummaryDialog/App.xaml.cs
@@ -66,9 +66,18 @@ namespace AutopilotMonitor.SummaryDialog
var exePath = System.Reflection.Assembly.GetExecutingAssembly().Location;
var exeDir = Path.GetDirectoryName(exePath);
- // Only self-cleanup if running from a temp folder
+ // Only self-cleanup if running from a known staging folder
+ // (agent copies the dialog to ProgramData\AutopilotMonitor\Summary-* or legacy TEMP)
+ if (exeDir == null)
+ return;
+
+ var programData = Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData);
+ var agentStagingRoot = Path.Combine(programData, "AutopilotMonitor");
var tempRoot = Path.GetTempPath().TrimEnd('\\');
- if (exeDir == null || !exeDir.StartsWith(tempRoot, StringComparison.OrdinalIgnoreCase))
+
+ var isStagedCopy = exeDir.StartsWith(agentStagingRoot, StringComparison.OrdinalIgnoreCase)
+ || exeDir.StartsWith(tempRoot, StringComparison.OrdinalIgnoreCase);
+ if (!isStagedCopy)
return;
var pid = Process.GetCurrentProcess().Id;