[CmdletBinding()] param( [ValidateRange(15, 10080)] [int]$MinAgeMinutes = 120, [switch]$Apply ) $ErrorActionPreference = 'Stop' throw 'Docker development copy: runtime control is disabled until container isolation is ready. See DOCKER_MIGRATION.md.' $now = Get-Date $agentVitePattern = 'AgentUI_2\.01.*frontend-next.*vite\\bin\\vite\.js' $npxVitePattern = 'npx-cli\.js.*\bvite\b' $agentEsbuildPattern = 'AgentUI_2\.01.*frontend-next.*@esbuild' function Get-VitePort { param([string]$CommandLine) if ($CommandLine -match '--port\s+(\d+)') { return $matches[1] } return $null } $processes = @(Get-CimInstance Win32_Process) $agentNodes = @( $processes | Where-Object { $_.Name -eq 'node.exe' -and [string]$_.CommandLine -match $agentVitePattern } ) $ports = @{} foreach ($process in $agentNodes) { $port = Get-VitePort ([string]$process.CommandLine) if ($port) { $ports[$port] = $true } } $npxNodes = @( $processes | Where-Object { if ($_.Name -ne 'node.exe' -or [string]$_.CommandLine -notmatch $npxVitePattern) { return $false } $port = Get-VitePort ([string]$_.CommandLine) return $port -and $ports.ContainsKey($port) } ) $connectedPids = @{} try { Get-NetTCPConnection -State Established -ErrorAction Stop | ForEach-Object { $connectedPids[[int]$_.OwningProcess] = $true } } catch { throw "Refusing cleanup because established TCP connections could not be inspected: $($_.Exception.Message)" } $protectedPorts = @{} $newestAgent = $agentNodes | Sort-Object CreationDate -Descending | Select-Object -First 1 if ($newestAgent) { $newestPort = Get-VitePort ([string]$newestAgent.CommandLine) if ($newestPort) { $protectedPorts[$newestPort] = 'newest AgentUI Vite instance' } } foreach ($process in @($agentNodes + $npxNodes)) { $port = Get-VitePort ([string]$process.CommandLine) if (-not $port) { continue } $created = [datetime]$process.CreationDate $ageMinutes = ($now - $created).TotalMinutes if ($ageMinutes -lt $MinAgeMinutes) { $protectedPorts[$port] = "younger than $MinAgeMinutes minutes" } if ($connectedPids.ContainsKey([int]$process.ProcessId)) { $protectedPorts[$port] = 'has an established TCP connection' } } $targetAgentNodes = @( $agentNodes | Where-Object { $port = Get-VitePort ([string]$_.CommandLine) $port -and -not $protectedPorts.ContainsKey($port) } ) $targetPorts = @{} $targetAgentPids = @{} foreach ($process in $targetAgentNodes) { $targetPorts[(Get-VitePort ([string]$process.CommandLine))] = $true $targetAgentPids[[int]$process.ProcessId] = $true } $targetNpxNodes = @( $npxNodes | Where-Object { $port = Get-VitePort ([string]$_.CommandLine) $port -and $targetPorts.ContainsKey($port) } ) $targetEsbuild = @( $processes | Where-Object { $_.Name -eq 'esbuild.exe' -and [string]$_.ExecutablePath -match $agentEsbuildPattern -and $targetAgentPids.ContainsKey([int]$_.ParentProcessId) } ) $targetIds = @( @($targetEsbuild + $targetAgentNodes + $targetNpxNodes).ProcessId | Where-Object { $null -ne $_ -and [int]$_ -gt 0 } | ForEach-Object { [int]$_ } | Sort-Object -Unique ) Write-Output "AgentUI Vite instances: $($agentNodes.Count)" Write-Output "Protected ports: $($protectedPorts.Count)" Write-Output "Stale ports: $($targetPorts.Count)" Write-Output "Target processes: $($targetIds.Count)" if (-not $Apply) { Write-Output 'Dry run only. Re-run with -Apply to terminate the exact stale process set.' exit 0 } if ($targetIds.Count -eq 0) { Write-Output 'No stale AgentUI Vite processes require cleanup.' exit 0 } Stop-Process -Id $targetIds -Force -ErrorAction SilentlyContinue Start-Sleep -Seconds 5 $remaining = @( Get-CimInstance Win32_Process | Where-Object { $targetIds -contains [int]$_.ProcessId -and ( ($_.Name -eq 'node.exe' -and ( [string]$_.CommandLine -match $agentVitePattern -or [string]$_.CommandLine -match $npxVitePattern )) -or ($_.Name -eq 'esbuild.exe' -and [string]$_.ExecutablePath -match $agentEsbuildPattern) ) } ) if ($remaining.Count -gt 0) { Write-Error "Cleanup left $($remaining.Count) exact target process(es) running." exit 1 } Write-Output "Terminated $($targetIds.Count) stale AgentUI Vite process(es)."