Private/Invoke-SPMWithRetry.ps1

function Invoke-SPMProgress {
    <#
    .SYNOPSIS
        Writes scan progress to the console AND to an optional caller-supplied callback
        ($script:SPMProgressAction) so hosts like the cloud worker can surface live status.
    #>

    [CmdletBinding()]
    param(
        [int]$Id = 2,
        [int]$ParentId = -1,
        [string]$Activity = 'Scanning structure',
        [string]$Status,
        [int]$PercentComplete = -1,
        [switch]$Completed
    )

    if ($Completed) {
        Write-Progress -Id $Id -Activity $Activity -Completed
        return
    }
    $wp = @{ Id = $Id; Activity = $Activity; Status = $Status }
    if ($ParentId -ge 0) { $wp.ParentId = $ParentId }
    if ($PercentComplete -ge 0) { $wp.PercentComplete = $PercentComplete }
    Write-Progress @wp
    if ($script:SPMProgressAction) {
        try { & $script:SPMProgressAction $Status } catch { Write-Verbose "Progress callback failed: $($_.Exception.Message)" }
    }
}

function Invoke-SPMWithRetry {
    <#
    .SYNOPSIS
        Executes a script block and retries on SharePoint/Graph throttling (HTTP 429/503), honoring Retry-After.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory, Position = 0)]
        [scriptblock]$ScriptBlock,

        [int]$MaxRetries = 6
    )

    $attempt = 0
    while ($true) {
        try {
            return & $ScriptBlock
        }
        catch {
            $attempt++
            $status = $null
            $retryAfter = $null

            $response = $_.Exception.Response
            if ($response) {
                try { $status = [int]$response.StatusCode } catch { $status = $null }
                try {
                    $values = @($response.Headers.GetValues('Retry-After'))
                    if ($values.Count -gt 0) { $retryAfter = $values[0] }
                }
                catch { $retryAfter = $null }
            }
            if (-not $status -and $_.Exception.Message -match '(?i)429|too many requests|throttl|503|server too busy') {
                $status = 429
            }

            if (($status -in 429, 503) -and $attempt -le $MaxRetries) {
                $delay = [Math]::Min(10 * $attempt, 120)
                $parsed = 0
                if ($retryAfter -and [int]::TryParse([string]$retryAfter, [ref]$parsed) -and $parsed -gt 0) {
                    $delay = $parsed
                }
                Write-Warning "Throttled (HTTP $status) - attempt $attempt/$MaxRetries, waiting $delay seconds..."
                Start-Sleep -Seconds $delay
                continue
            }

            throw
        }
    }
}