Public/Watch-DebateProgress.ps1
|
# Copyright (c) 2026 Jeffrey Snover. All rights reserved. # Licensed under the MIT License. See LICENSE file in the project root. function Watch-DebateProgress { <# .SYNOPSIS Live-updating display of a debate batch's progress file. .DESCRIPTION Polls a debate-progress.json file written by Invoke-DebateBatch / Invoke-AITDebate -ProgressFile, and renders a table of per-debate status. Refreshes every -IntervalSeconds (default 10s). Press Ctrl+C to exit. A debate whose last_update_at is older than -HungAfterMinutes (default 5) while still marked 'running' is flagged HUNG? in red — useful for catching silent stalls like the 3-hour exp-1069 hang. .PARAMETER Path Path to the progress file. Defaults to ./debate-progress.json under the current directory. .PARAMETER IntervalSeconds Poll interval (1-300, default 10). .PARAMETER HungAfterMinutes Threshold in minutes after which a 'running' debate without updates is flagged HUNG? (1-60, default 5). .EXAMPLE Watch-DebateProgress .EXAMPLE Watch-DebateProgress -Path D:\debates\exp-1069\debate-progress.json -IntervalSeconds 5 .LINK Show-AITriadHelp .LINK Show-TriadDialogue .LINK Invoke-AITDebate .LINK Get-AITDebate .LINK Resume-AITDebate .LINK Repair-DebateOutput .LINK Invoke-DebateBatch #> [CmdletBinding()] param( [Parameter(Position = 0)] [string]$Path = (Join-Path (Get-Location) 'debate-progress.json'), [ValidateRange(1, 300)] [int]$IntervalSeconds = 10, [ValidateRange(1, 60)] [int]$HungAfterMinutes = 5 ) Set-StrictMode -Version Latest Write-Host "Watching: $Path" -ForegroundColor Cyan Write-Host "Refresh: ${IntervalSeconds}s | Hung threshold: ${HungAfterMinutes}m | Ctrl+C to exit" -ForegroundColor DarkGray Write-Host '' while ($true) { if (-not (Test-Path $Path)) { Clear-Host Write-Host "Waiting for progress file: $Path" -ForegroundColor Yellow Start-Sleep -Seconds $IntervalSeconds continue } $State = $null try { $Raw = Get-Content -Raw -Path $Path -ErrorAction Stop if (-not [string]::IsNullOrWhiteSpace($Raw)) { $State = $Raw | ConvertFrom-Json } } catch { # Reader race against atomic-rename writer is unlikely but harmless — try next tick Start-Sleep -Seconds $IntervalSeconds continue } if (-not $State) { Start-Sleep -Seconds $IntervalSeconds continue } Clear-Host # Header $Now = Get-Date $BatchName = if ($State.PSObject.Properties['batch_name']) { $State.batch_name } else { '(unnamed)' } $Elapsed = '' if ($State.PSObject.Properties['started_at'] -and $State.started_at) { try { $Started = [DateTimeOffset]::Parse($State.started_at).UtcDateTime $Span = $Now.ToUniversalTime() - $Started $Elapsed = " (running {0:hh\:mm\:ss})" -f $Span } catch { } } Write-Host ("[{0:HH:mm:ss}] Debate Batch: {1}{2}" -f $Now, $BatchName, $Elapsed) -ForegroundColor Cyan Write-Host '' # Table $Debates = @() if ($State.PSObject.Properties['debates']) { $Debates = @($State.debates) } if ($Debates.Count -eq 0) { Write-Host ' (no debates registered yet)' -ForegroundColor DarkGray } else { $NameWidth = [Math]::Max(20, (@($Debates | ForEach-Object { $_.name.Length }) | Measure-Object -Maximum).Maximum) $Fmt = " {0,-$NameWidth} {1,-9} {2,-6} {3,-9} {4,-15} {5}" Write-Host ($Fmt -f 'Name', 'Status', 'Turn', 'Stage', 'Debater', 'Last Update') -ForegroundColor White Write-Host ($Fmt -f ('-' * $NameWidth), '-------', '----', '-----', '-------', '-----------') -ForegroundColor DarkGray foreach ($D in $Debates) { $Name = $D.name $Status = if ($D.PSObject.Properties['status']) { [string]$D.status } else { 'unknown' } $Turn = '-' if ($D.PSObject.Properties['current_turn'] -and $D.PSObject.Properties['total_turns_expected'] -and $D.total_turns_expected) { $Turn = "$($D.current_turn)/$($D.total_turns_expected)" } elseif ($D.PSObject.Properties['current_turn']) { $Turn = [string]$D.current_turn } $Stage = if ($D.PSObject.Properties['current_stage']) { [string]$D.current_stage } else { '-' } $Debater = if ($D.PSObject.Properties['current_debater']) { [string]$D.current_debater } else { '-' } # Compute last_update relative time $LastUpdateText = '-' $IsHung = $false if ($D.PSObject.Properties['last_update_at'] -and $D.last_update_at) { try { $LastUpdate = [DateTimeOffset]::Parse($D.last_update_at).UtcDateTime $Age = $Now.ToUniversalTime() - $LastUpdate $LastUpdateText = if ($Age.TotalSeconds -lt 60) { "{0:n0}s ago" -f $Age.TotalSeconds } elseif ($Age.TotalMinutes -lt 60) { "{0:n0}m ago" -f $Age.TotalMinutes } else { "{0:hh\:mm\:ss} ago" -f $Age } if ($Status -eq 'running' -and $Age.TotalMinutes -gt $HungAfterMinutes) { $IsHung = $true } } catch { } } $StatusText = if ($IsHung) { "HUNG? " } else { $Status } $Color = switch ($Status) { 'done' { 'Green' } 'failed' { 'Red' } 'running' { if ($IsHung) { 'Red' } else { 'Yellow' } } 'pending' { 'DarkGray' } default { 'White' } } Write-Host ($Fmt -f $Name, $StatusText, $Turn, $Stage, $Debater, $LastUpdateText) -ForegroundColor $Color } # Footer counts $StatusCounts = @{} foreach ($D in $Debates) { $S = if ($D.PSObject.Properties['status']) { [string]$D.status } else { 'unknown' } if (-not $StatusCounts.ContainsKey($S)) { $StatusCounts[$S] = 0 } $StatusCounts[$S]++ } $Summary = ($StatusCounts.Keys | Sort-Object | ForEach-Object { "$($_)=$($StatusCounts[$_])" }) -join ' ' Write-Host '' Write-Host " $Summary" -ForegroundColor DarkGray } Start-Sleep -Seconds $IntervalSeconds } } |