diff --git a/relay/clients/igneum-agent.ps1 b/relay/clients/igneum-agent.ps1 index 68b7437c4..7554e8ada 100644 --- a/relay/clients/igneum-agent.ps1 +++ b/relay/clients/igneum-agent.ps1 @@ -282,13 +282,25 @@ exit `$code $args = @('-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', "`"$wrap`"") $code = 1 try { + # the wait is on the task's own process with a cap, never -Wait (PC 2, 8 October 2026, 21:28 UK: -Wait covers every + # descendant on the inherited console, so a task that started Igneum Miner held the agent for the app's whole life; the + # relay went silent for half an hour). The cap is the task's timeout (flags.timeout_minutes, default 60); a task that + # runs past it is ended by ITS pid and the result says so. + $capMin = 60; try { if ($task.flags.timeout_minutes) { $capMin = [int]$task.flags.timeout_minutes } } catch {} if ($elevated -and -not (Is-Admin)) { Log 'task needs administrator and the agent is not elevated: asking (UAC prompt on this PC)' - $p = Start-Process powershell.exe -ArgumentList $args -Verb RunAs -Wait -PassThru + $p = Start-Process powershell.exe -ArgumentList $args -Verb RunAs -PassThru } else { - $p = Start-Process powershell.exe -ArgumentList $args -NoNewWindow -Wait -PassThru + $p = Start-Process powershell.exe -ArgumentList $args -NoNewWindow -PassThru + } + if (-not $p.WaitForExit($capMin * 60 * 1000)) { + Log ("task #" + $id + " ran past its cap of " + $capMin + " min: ending its own shell pid " + $p.Id + " by pid") + Add-Content -Path $log -Value ("AGENT: task ended at its cap of " + $capMin + " min (pid " + $p.Id + ")") + Stop-Process -Id $p.Id -Force -ErrorAction SilentlyContinue + $p.WaitForExit(5000) | Out-Null } $code = $p.ExitCode + if ($null -eq $code) { $code = 124 } } catch { Log ("could not start the task: " + $_.Exception.Message); Add-Content -Path $log -Value ("AGENT ERROR: " + $_.Exception.Message) } $text = ''; if (Test-Path $log) { $text = Get-Content $log -Raw } # the marker on a line of its own (X28), and only when the task was queued with --reboot or --reboot-continue diff --git a/relay/test/service.test.mjs b/relay/test/service.test.mjs index 357efc61c..214879e78 100644 --- a/relay/test/service.test.mjs +++ b/relay/test/service.test.mjs @@ -39,6 +39,11 @@ test('the installer fills the manifest and registers, runs and reads back the ta assert.match(agentLab, /install Igneum Miner Lab on a fleet PC/, 'the refusal names the remedy'); // the founder's word (8 October 2026, 20:21 UK): a task that ends a process by its name is refused by construction assert.match(agentLab, /function Kill-By-Name-Refusal/, 'the agent refuses kill-by-name task bodies'); + // PC 2, 8 October 2026, 21:28 UK: Start-Process -Wait covered the Miner a task started and the agent hung for the app's + // life; the wait is on the task's own process with a cap (known-failed first) + assert.doesNotMatch(agentLab, /Start-Process powershell\.exe -ArgumentList \$args[^\n]*-Wait/, 'never -Wait on a task shell'); + assert.match(agentLab, /\$p\.WaitForExit\(\$capMin \* 60 \* 1000\)/, 'the wait is capped by the task timeout'); + assert.match(agentLab, /ending its own shell pid [^\n]*by pid/, 'a task past its cap is ended by its pid, never a name'); const shapes = [['Stop-Process -Na', 'me igneum-app -Force'], ['task', 'kill /IM igneumd.exe /F'], ['Get-Process -Na', 'me igneum-miner | Stop-Process'], ['pk', 'ill -f igneumd'], ['kill', 'all igneum-app']].map(p => p.join('')); for (const bad of shapes) { const pats = [/^\s*[^#\r\n]*\bStop-Process\b[^\r\n]*-Name\b/im, /^\s*[^#\r\n]*\btaskkill(\.exe)?\b[^\r\n]*\/IM\b/im, /^\s*[^#\r\n]*\bGet-Process\b[^\r\n]*-Name\b[^\r\n]*\|\s*Stop-Process/im, /^\s*[^#\r\n]*\b(pkill|killall)\b/im];