diff --git a/scripts/Bootstrap/Install-AutopilotMonitor.ps1 b/scripts/Bootstrap/Install-AutopilotMonitor.ps1
index 294efd0a..f803d214 100644
--- a/scripts/Bootstrap/Install-AutopilotMonitor.ps1
+++ b/scripts/Bootstrap/Install-AutopilotMonitor.ps1
@@ -31,7 +31,7 @@
.NOTES
- Agent is temporary and auto-removes after enrollment
- Everything in C:\ProgramData\AutopilotMonitor (easy cleanup)
- - No Registry entries (clean machine after removal)
+ - One Registry key left after removal: HKLM\SOFTWARE\AutopilotMonitor\Deployed (prevents ghost re-installs)
- Scheduled Task survives reboots during enrollment
- SHA-256 integrity verification since v1.0.706+
- IMPORTANT: This script MUST remain pure ASCII (no Unicode/UTF-8 special chars).
@@ -53,9 +53,9 @@
param(
[Parameter(Mandatory = $false)]
[string]$AgentDownloadUrl = "https://autopilotmonitor.blob.core.windows.net/agent/AutopilotMonitor-Agent.zip",
-
+
[Parameter(Mandatory = $false)]
- [int]$MaxOsAgeMinutes = 120
+ [int]$MaxBootstrapWindowHours = 12
)
# Configuration - Everything in ProgramData for easy cleanup
@@ -103,61 +103,75 @@ function Get-FileMd5Base64 {
try {
Write-Log "===== Autopilot Monitor Bootstrap Started ====="
- # -- Pre-flight: Skip installation on already-enrolled devices --
+ # -- Pre-flight: Multi-signal guard to prevent installation on non-provisioning devices --
+ # Layered approach: each guard catches a different scenario.
+ # Guard 1: Ghost re-installs (registry marker from previous deployment)
+ # Guard 2: Productive devices not in OOBE
+ # Guard 3: Productive devices with stale OOBEInProgress flag (real user profile exists)
+ # Guard 4: Ultra edge case (no key, OOBE stuck, no user, but device running too long)
+ # Guard 5: Agent binary already present from a previous run
- # Check 1: OS install date must be within threshold (fresh device)
- # TODO: issue is the OS might got installed a long time ago but the OOBE just started now (e.g. old image, or re-flash).
- # Ideally we want to detect the OOBE start time... need to investigate if there's a reliable way to get that (event log? registry? WMI?).
- # For now we just check OS install date as a proxy, which should work in most cases except for old images or re-flashed devices.
- $osInstallDate = (Get-CimInstance Win32_OperatingSystem).InstallDate
- $osAge = (Get-Date) - $osInstallDate
- $osAgeMinutesRounded = [int][Math]::Round($osAge.TotalMinutes)
- Write-Log "OS install date: $osInstallDate (age: ${osAgeMinutesRounded}m)"
-
- if ($osAge.TotalMinutes -gt $MaxOsAgeMinutes) {
- Write-Log "SKIP: OS was installed ${osAgeMinutesRounded}m ago (threshold: ${MaxOsAgeMinutes}m). Device is not freshly provisioned."
+ # Guard 1: Agent was already deployed on this device (registry marker survives self-destruct)
+ $deployed = (Get-ItemProperty -Path 'HKLM:\SOFTWARE\AutopilotMonitor' -Name 'Deployed' -ErrorAction SilentlyContinue).Deployed
+ if ($deployed) {
+ Write-Log "SKIP: Agent was previously deployed at $deployed."
exit 0
}
- # Check 2: MDM enrollment must not be completed yet
- $enrollmentPath = "HKLM:\SOFTWARE\Microsoft\Enrollments"
- $mdmEnrolled = $false
-
- if (Test-Path $enrollmentPath) {
- $enrollmentEntries = Get-ChildItem -Path $enrollmentPath -ErrorAction SilentlyContinue |
- Where-Object { $_.PSChildName -match '^[0-9A-Fa-f\-]{36}$' }
-
- foreach ($entry in $enrollmentEntries) {
- $providerID = (Get-ItemProperty -Path $entry.PSPath -Name "ProviderID" -ErrorAction SilentlyContinue).ProviderID
- if ($providerID) {
- $mdmEnrolled = $true
- Write-Log "Found MDM enrollment: ProviderID=$providerID (EnrollmentID=$($entry.PSChildName))"
- break
- }
- }
+ # Guard 2: Device must be in OOBE (= actively provisioning)
+ $oobeInProgress = (Get-ItemProperty -Path 'HKLM:\SYSTEM\Setup' -Name 'OOBEInProgress' -ErrorAction SilentlyContinue).OOBEInProgress
+ Write-Log "OOBEInProgress=$oobeInProgress"
+ if ($oobeInProgress -ne 1) {
+ Write-Log "SKIP: Device is not in OOBE (OOBEInProgress=$oobeInProgress). Not a fresh enrollment."
+ exit 0
}
- if ($mdmEnrolled) {
- # Double-check: Is enrollment status tracking showing completed?
- $espPath = "HKLM:\SOFTWARE\Microsoft\Windows\Autopilot\EnrollmentStatusTracking\Device\Setup"
- $hasCompleted = (Get-ItemProperty -Path $espPath -Name "HasProvisioningCompleted" -ErrorAction SilentlyContinue).HasProvisioningCompleted
+ # Guard 3: No real user profile should exist yet (catches stale OOBEInProgress on productive devices)
+ # Combines WMI (Win32_UserProfile.Special flag) and filesystem for maximum reliability.
+ $excludePattern = '^(defaultuser\d*|Public|Default( User)?|All Users)$'
- if ($hasCompleted -eq 1) {
- Write-Log "SKIP: Autopilot enrollment already completed (HasProvisioningCompleted=1). Device is already enrolled."
- exit 0
- }
+ $profileNames = @(
+ # WMI/CIM view -- Special flag reliably excludes SYSTEM/LocalService/NetworkService
+ try {
+ Get-CimInstance Win32_UserProfile -ErrorAction Stop |
+ Where-Object { -not $_.Special -and $_.LocalPath -like 'C:\Users\*' } |
+ ForEach-Object { Split-Path $_.LocalPath -Leaf }
+ } catch { Write-Log "INFO: WMI profile query failed, continuing with filesystem check." }
+
+ # Filesystem view -- catches profiles WMI might miss
+ (Get-ChildItem 'C:\Users' -Directory -ErrorAction SilentlyContinue).Name
+ ) | Where-Object { $_ -and $_ -notmatch $excludePattern } |
+ Select-Object -Unique
+
+ if ($profileNames) {
+ $names = ($profileNames | Select-Object -First 3) -join ', '
+ Write-Log "SKIP: Real user profile(s) found ($names). Device appears productive."
+ exit 0
}
- # Check 3: Is the agent already installed? (leftover from previous run)
+ # Guard 4: Bootstrap window check (ultra edge case: no key, OOBE stuck, no user profiles)
+ # NOT "how long may enrollment take" -- agent handles that internally (6h emergency break).
+ # This is "how old can the OOBE state be before I no longer trust it for initial install".
+ # 12h bootstrap window vs 6h agent emergency break = consistent layered approach.
+ # Sleep/standby does NOT reset uptime -- only real boot/restart does.
+ $lastBoot = (Get-CimInstance Win32_OperatingSystem).LastBootUpTime
+ $uptimeHours = ((Get-Date) - $lastBoot).TotalHours
+ Write-Log "Device uptime: $([int]$uptimeHours)h (last boot: $lastBoot)"
+ if ($uptimeHours -gt $MaxBootstrapWindowHours) {
+ Write-Log "SKIP: Device uptime is $([int]$uptimeHours)h. OOBE state is older than accepted bootstrap window of ${MaxBootstrapWindowHours}h."
+ exit 0
+ }
+
+ # Guard 5: Is the agent already installed? (leftover from previous run)
if (Test-Path $AgentBinPath) {
$existingAgent = Get-ChildItem -Path $AgentBinPath -Filter "AutopilotMonitor.Agent.exe" -ErrorAction SilentlyContinue
if ($existingAgent) {
- Write-Log "SKIP: Agent already installed at $($existingAgent.FullName). Previous enrollment may have been in progress. Please check if enrollment completed or clean up C:\ProgramData\AutopilotMonitor before re-running."
+ Write-Log "SKIP: Agent already installed at $($existingAgent.FullName)."
exit 0
}
}
- Write-Log "Pre-flight checks passed - device is freshly provisioned and enrollment in progress"
+ Write-Log "Pre-flight checks passed -- device is in OOBE, no prior deployment, no real user profiles"
# Download and extract agent binaries
$agentExePath = Join-Path $AgentBinPath "AutopilotMonitor.Agent.exe"
diff --git a/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Collectors/EspAndHelloTracker.HelloTracking.cs b/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Collectors/EspAndHelloTracker.HelloTracking.cs
index 88ef3e31..dc9f9c0c 100644
--- a/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Collectors/EspAndHelloTracker.HelloTracking.cs
+++ b/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Collectors/EspAndHelloTracker.HelloTracking.cs
@@ -390,6 +390,28 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Collectors
}
}
+ ///
+ /// Resets Hello tracking state when ESP resumes after a mid-enrollment reboot (hybrid join).
+ /// Stops all running timers and clears Hello state so the timer chain restarts fresh when
+ /// ESP exits again for real.
+ ///
+ public void ResetForEspResumption()
+ {
+ lock (_stateLock)
+ {
+ _helloWaitTimer?.Dispose();
+ _helloWaitTimer = null;
+ _helloCompletionTimer?.Dispose();
+ _helloCompletionTimer = null;
+
+ _isHelloCompleted = false;
+ _helloWizardStarted = false;
+ _espExitDetected = false;
+ HelloOutcome = null;
+ }
+ _logger.Info("EspAndHelloTracker: Reset for ESP resumption — Hello tracking restarted");
+ }
+
///
/// Starts the Hello wait timer. Should be called by EnrollmentTracker when AccountSetup phase exits.
/// The timer waits for Hello wizard to start (event 62404) within the configured timeout.
diff --git a/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Tracking/EnrollmentStatePersistence.cs b/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Tracking/EnrollmentStatePersistence.cs
index 57c694a6..72446710 100644
--- a/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Tracking/EnrollmentStatePersistence.cs
+++ b/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Tracking/EnrollmentStatePersistence.cs
@@ -117,6 +117,9 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Tracking
// True when AAD join status shows a joined device with a real user email
public bool AadJoinedWithUser { get; set; }
+ // True when Autopilot profile indicates Hybrid Azure AD Join (CloudAssignedDomainJoinMethod == 1)
+ public bool IsHybridJoin { get; set; }
+
// Signal timestamps for audit trail
public DateTime? EspFirstSeenUtc { get; set; }
public DateTime? EspFinalExitUtc { get; set; }
diff --git a/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Tracking/EnrollmentTracker.CompletionLogic.cs b/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Tracking/EnrollmentTracker.CompletionLogic.cs
index 424f0166..420a9d10 100644
--- a/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Tracking/EnrollmentTracker.CompletionLogic.cs
+++ b/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Tracking/EnrollmentTracker.CompletionLogic.cs
@@ -550,6 +550,8 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Tracking
_skipDeviceStatusPage = loaded.SkipDeviceStatusPage;
_autopilotMode = loaded.AutopilotMode;
_aadJoinedWithUser = loaded.AadJoinedWithUser;
+ // Restore hybrid join flag from persisted state; re-detect as fallback
+ _isHybridJoin = loaded.IsHybridJoin || DetectHybridJoinStatic();
_stateData = loaded;
}
@@ -632,6 +634,7 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Tracking
// and OnWaitingForHelloSafetyTimeout) both read _enrollmentCompleteEmitted=false simultaneously.
bool helloResolved;
bool espGateBlocked;
+ bool hybridRebootGateBlocked;
bool alreadyEmitted;
string enrollmentType;
bool espEverSeen, espFinalExitSeen, desktopArrived;
@@ -646,6 +649,7 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Tracking
alreadyEmitted = true;
helloResolved = false;
espGateBlocked = false;
+ hybridRebootGateBlocked = false;
enrollmentType = _enrollmentType;
espEverSeen = _espEverSeen;
espFinalExitSeen = _espFinalExitSeen;
@@ -674,8 +678,23 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Tracking
espGateBlocked = (source == "desktop_arrival" || source == "desktop_hello")
&& enrollmentType != "v2" && espEverSeen && !espFinalExitSeen;
+ // GUARD 3: Hybrid Join reboot gate
+ // In hybrid join, ESP exit may be for a mid-enrollment reboot (domain user
+ // login required). Block esp_hello_composite unless we have stronger confirmation:
+ // - IME user session completed (strongest signal: IME says enrollment is done)
+ // - Agent restarted after ESP exit (reboot happened, we survived it)
+ hybridRebootGateBlocked = false;
+ if (_isHybridJoin && source == "esp_hello_composite")
+ {
+ bool hasImeCompletion = _stateData.ImePatternSeenUtc.HasValue;
+ bool agentRestartedAfterEspExit = _stateData.EspFinalExitUtc.HasValue
+ && _agentStartTimeUtc > _stateData.EspFinalExitUtc.Value;
+
+ hybridRebootGateBlocked = !hasImeCompletion && !agentRestartedAfterEspExit;
+ }
+
// If all guards pass, claim the flag atomically
- if (helloResolved && !espGateBlocked)
+ if (helloResolved && !espGateBlocked && !hybridRebootGateBlocked)
{
_enrollmentCompleteEmitted = true;
_stateData.EnrollmentCompleteEmitted = true;
@@ -698,7 +717,8 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Tracking
_logger.Debug($"EnrollmentTracker: TryEmitEnrollmentComplete('{source}') — evaluating guards " +
$"[espEverSeen={espEverSeen}, espFinalExitSeen={espFinalExitSeen}, desktopArrived={desktopArrived}, " +
$"helloCompleted={_espAndHelloTracker?.IsHelloCompleted ?? false}, helloPolicyConfigured={_espAndHelloTracker?.IsPolicyConfigured ?? false}, " +
- $"enrollmentType={enrollmentType}, autopilotMode={autopilotMode}, isSelfDeploying={isSelfDeploying}, isDeviceOnlyDeployment={isDeviceOnly}]");
+ $"enrollmentType={enrollmentType}, autopilotMode={autopilotMode}, isSelfDeploying={isSelfDeploying}, isDeviceOnlyDeployment={isDeviceOnly}, " +
+ $"isHybridJoin={_isHybridJoin}]");
if (!helloResolved)
{
@@ -714,6 +734,14 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Tracking
return;
}
+ if (hybridRebootGateBlocked)
+ {
+ _logger.Info($"EnrollmentTracker: Completion source '{source}' blocked — hybrid join reboot gate " +
+ $"(waiting for IME completion or agent restart after ESP exit)");
+ EmitCompletionCheck(source, "blocked", "hybrid_reboot_gate");
+ return;
+ }
+
_logger.Info($"EnrollmentTracker: all guards passed for source '{source}' — emitting enrollment_complete");
// Stop timers
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 06b06eb1..29eacec9 100644
--- a/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Tracking/EnrollmentTracker.EventHandlers.cs
+++ b/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Tracking/EnrollmentTracker.EventHandlers.cs
@@ -50,6 +50,45 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Tracking
return;
}
+ // ESP resumed after final exit (e.g., hybrid join reboot recovery).
+ // If ESP was marked as "final exited" but we see the same phase again from IME,
+ // ESP has resumed after a reboot. Reset completion state so the agent waits for the
+ // real final exit instead of self-destructing prematurely.
+ bool espFinalExitSeen;
+ lock (_stateLock) { espFinalExitSeen = _espFinalExitSeen; }
+
+ if (espFinalExitSeen && string.Equals(phase, lastPhase, StringComparison.OrdinalIgnoreCase))
+ {
+ _logger.Info($"EnrollmentTracker: ESP phase '{phase}' re-detected after final exit — " +
+ "ESP has resumed (hybrid join reboot recovery), resetting completion state");
+ lock (_stateLock)
+ {
+ _espFinalExitSeen = false;
+ _stateData.EspFinalExitSeen = false;
+ _stateData.EspFinalExitUtc = null;
+ }
+ RecordSignal("esp_resumed");
+ _espAndHelloTracker?.ResetForEspResumption();
+
+ _emitEvent(new EnrollmentEvent
+ {
+ SessionId = _sessionId,
+ TenantId = _tenantId,
+ EventType = "esp_resumed",
+ Severity = EventSeverity.Info,
+ Source = "EnrollmentTracker",
+ Phase = EnrollmentPhase.AccountSetup,
+ Message = $"ESP phase '{phase}' re-detected after final exit — enrollment resumed after reboot",
+ Data = new Dictionary
+ {
+ { "espPhase", phase },
+ { "isHybridJoin", _isHybridJoin }
+ },
+ ImmediateUpload = true
+ });
+ return;
+ }
+
// Only emit event if the phase has actually changed
if (string.Equals(phase, lastPhase, StringComparison.OrdinalIgnoreCase))
{
diff --git a/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Tracking/EnrollmentTracker.cs b/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Tracking/EnrollmentTracker.cs
index 04fb5243..a5b16152 100644
--- a/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Tracking/EnrollmentTracker.cs
+++ b/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Tracking/EnrollmentTracker.cs
@@ -72,6 +72,7 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Tracking
private bool _espFinalExitSeen;
private bool _enrollmentCompleteEmitted;
private bool _desktopArrived;
+ private bool _isHybridJoin;
private readonly DateTime _agentStartTimeUtc = DateTime.UtcNow;
// Device-Only ESP detection (Phase 3C)
@@ -189,6 +190,7 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Tracking
{
_isBootstrapMode = isBootstrapMode;
_sendTraceEvents = sendTraceEvents;
+ _isHybridJoin = DetectHybridJoinStatic();
_sessionId = sessionId;
_tenantId = tenantId;
_emitEvent = emitEvent;
@@ -359,6 +361,7 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Tracking
_stateData.SkipDeviceStatusPage = _skipDeviceStatusPage;
_stateData.AutopilotMode = _autopilotMode;
_stateData.AadJoinedWithUser = _aadJoinedWithUser;
+ _stateData.IsHybridJoin = _isHybridJoin;
_stateDirty = true;
}
diff --git a/src/Agent/AutopilotMonitor.Agent/Program.Configuration.cs b/src/Agent/AutopilotMonitor.Agent/Program.Configuration.cs
index aad79c04..68339fcb 100644
--- a/src/Agent/AutopilotMonitor.Agent/Program.Configuration.cs
+++ b/src/Agent/AutopilotMonitor.Agent/Program.Configuration.cs
@@ -222,6 +222,56 @@ namespace AutopilotMonitor.Agent
///
static bool CheckEnrollmentCompleteMarker(AgentConfiguration config, AgentLogger logger, bool consoleMode)
{
+ // Registry guard: if "Deployed" key exists but no active session file,
+ // this is a ghost restart after previous self-destruct. Exit immediately.
+ try
+ {
+ using (var regKey = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\AutopilotMonitor"))
+ {
+ var deployed = regKey?.GetValue("Deployed") as string;
+ if (!string.IsNullOrEmpty(deployed))
+ {
+ var dataDir = Environment.ExpandEnvironmentVariables(Constants.AgentDataDirectory);
+ var sessionPersistence = new SessionPersistence(dataDir);
+
+ if (!sessionPersistence.SessionExists())
+ {
+ logger.Info($"Agent was previously deployed ({deployed}) but no active session — ghost restart detected");
+
+ if (config.SelfDestructOnComplete)
+ {
+ logger.Info("Attempting cleanup retry (scheduled task may still be registered)...");
+ try
+ {
+ using (var service = new MonitoringService(config, logger, GetAgentVersion()))
+ {
+ service.TriggerCleanup();
+ }
+ logger.Info("Cleanup retry completed.");
+ }
+ catch (Exception cleanupEx)
+ {
+ logger.Warning($"Cleanup retry failed: {cleanupEx.Message}");
+ }
+ }
+
+ if (consoleMode)
+ Console.WriteLine("Agent was previously deployed but no active session. Exiting.");
+
+ return true;
+ }
+
+ // session.id exists → normal restart (Part 2, crash recovery) → continue
+ logger.Info($"Deployment marker found ({deployed}) with active session — continuing normally");
+ }
+ }
+ }
+ catch (Exception regEx)
+ {
+ logger.Warning($"Registry deployment marker check failed: {regEx.Message}");
+ }
+
+ // File-based enrollment-complete marker (original guard)
var stateDirectory = Environment.ExpandEnvironmentVariables(Constants.StateDirectory);
var markerPath = Path.Combine(stateDirectory, "enrollment-complete.marker");
diff --git a/src/Agent/AutopilotMonitor.Agent/Program.InstallMode.cs b/src/Agent/AutopilotMonitor.Agent/Program.InstallMode.cs
index 51112568..86d283b1 100644
--- a/src/Agent/AutopilotMonitor.Agent/Program.InstallMode.cs
+++ b/src/Agent/AutopilotMonitor.Agent/Program.InstallMode.cs
@@ -6,6 +6,7 @@ using System.Reflection;
using AutopilotMonitor.Agent.Core.Configuration;
using AutopilotMonitor.Agent.Core.Logging;
using AutopilotMonitor.Shared;
+using Microsoft.Win32;
namespace AutopilotMonitor.Agent
{
@@ -103,6 +104,21 @@ namespace AutopilotMonitor.Agent
logger.Info($"Scheduled Task '{taskName}' started successfully");
+ // Write deployment marker — survives self-destruct, prevents ghost re-installs.
+ // Written here (not in bootstrap script) so manual --install also sets it.
+ try
+ {
+ using (var regKey = Registry.LocalMachine.CreateSubKey(@"SOFTWARE\AutopilotMonitor"))
+ {
+ regKey.SetValue("Deployed", DateTime.UtcNow.ToString("O"));
+ }
+ logger.Info("Deployment registry marker written (HKLM\\SOFTWARE\\AutopilotMonitor\\Deployed)");
+ }
+ catch (Exception regEx)
+ {
+ logger.Warning($"Failed to write deployment registry marker: {regEx.Message}");
+ }
+
if (consoleMode)
{
Console.WriteLine("Installation completed successfully.");
diff --git a/src/Agent/AutopilotMonitor.Agent/buildcounter.txt b/src/Agent/AutopilotMonitor.Agent/buildcounter.txt
index b7ecf5c0..5d0df6f2 100644
--- a/src/Agent/AutopilotMonitor.Agent/buildcounter.txt
+++ b/src/Agent/AutopilotMonitor.Agent/buildcounter.txt
@@ -1 +1 @@
-734
+735