Odin/scripts/health-voidbot.ps1
Meta Vens 515a864ef0 Rebase Idunn actuator scripts off the projects root
The previous commit fixed the target catalog and the operator alarm, but the
actuators the catalog invokes carried the same E: paths. Idunn would have
resolved every target and then failed on the first restart or deploy it ran,
for exactly the reason the alarm failed.

Rebased 41 references across 27 scripts:

- .cmd bodies invoke their sibling .ps1 through %~dp0 instead of an absolute
  drive path, and restart-weksa.cmd reaches its sibling repo the same way.
- .ps1 parameter defaults and assignments derive from $PSScriptRoot: one level
  up for paths inside Odin, two for sibling repos under the projects root.

Left deliberately alone: $RemoteViliRoot and $StateRoot in restart-vili.ps1.
Both are consumed inside the EncodedCommand block that restart-vili ssh's to
raven, so they describe raven's filesystem, not starfire's. Whether raven also
moved off E: is an operator question, not something to infer from here.

Verified: all 46 scripts under scripts/ parse clean, and the derivation
resolves to F:\Projects\Odin and F:\Projects on this host.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 01:05:40 +02:00

181 lines
6.5 KiB
PowerShell

param(
[string] $VoidBotRoot = (Join-Path (Split-Path -Parent (Split-Path -Parent $PSScriptRoot)) "VoidBot"),
[int] $MaxOrchestratorAgeSeconds = 180,
[int] $MaxCultMeshStoreAgeSeconds = 180
)
Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"
function Fail-IdunnHealth {
param(
[string] $State,
[string] $Message
)
Write-Output "idunn.health.state=$State"
throw $Message
}
function Read-JsonFile {
param([Parameter(Mandatory = $true)][string] $Path)
if (-not (Test-Path -LiteralPath $Path)) {
return $null
}
$raw = Get-Content -LiteralPath $Path -Raw -Encoding UTF8
if ([string]::IsNullOrWhiteSpace($raw)) {
return $null
}
return $raw | ConvertFrom-Json
}
function Get-JsonPropertyValue {
param(
[Parameter(Mandatory = $false)] $Object,
[Parameter(Mandatory = $true)][string] $Name,
[Parameter(Mandatory = $false)] $Default = $null
)
if ($null -eq $Object) {
return $Default
}
$property = $Object.PSObject.Properties[$Name]
if ($null -eq $property) {
return $Default
}
return $property.Value
}
function Get-ProcessOrFail {
param(
[Parameter(Mandatory = $true)][string] $Role,
[Parameter(Mandatory = $true)][int] $ProcessId
)
$process = Get-Process -Id $ProcessId -ErrorAction SilentlyContinue
if ($null -eq $process) {
Fail-IdunnHealth -State "failed" -Message "VoidBot $Role process PID $ProcessId is not running."
}
return $process
}
function Assert-StoreContains {
param(
[Parameter(Mandatory = $true)][string] $StorePath,
[Parameter(Mandatory = $true)][string[]] $Tokens
)
$rg = (Get-Command rg.exe -ErrorAction SilentlyContinue).Source
if ([string]::IsNullOrWhiteSpace($rg)) {
Fail-IdunnHealth -State "degraded" -Message "ripgrep is required to inspect the VoidBot CultMesh witness store."
}
foreach ($token in $Tokens) {
& $rg -a -q --fixed-strings -- $token $StorePath
if ($LASTEXITCODE -ne 0) {
Fail-IdunnHealth -State "degraded" -Message "VoidBot CultMesh witness store is missing '$token'."
}
}
}
$runtimePath = Join-Path $VoidBotRoot ".voidbot\status\runtime-stack.json"
$orchestratorPath = Join-Path $VoidBotRoot ".voidbot\status\gamecult-orchestrator.json"
$cultMeshStorePath = Join-Path $VoidBotRoot ".voidbot\status\cultmesh\voidbot-swarm-state.cc"
$runtime = Read-JsonFile -Path $runtimePath
if ($null -eq $runtime) {
Fail-IdunnHealth -State "failed" -Message "VoidBot runtime state is missing at $runtimePath."
}
if (-not [bool](Get-JsonPropertyValue -Object $runtime -Name "ready" -Default $false)) {
Fail-IdunnHealth -State "failed" -Message "VoidBot runtime stack reports ready=false."
}
$runtimeStage = [string](Get-JsonPropertyValue -Object $runtime -Name "stage" -Default "unknown")
if ($runtimeStage -ne "ready") {
Fail-IdunnHealth -State "failed" -Message "VoidBot runtime stack stage is '$runtimeStage', expected 'ready'."
}
$bot = Get-JsonPropertyValue -Object $runtime -Name "bot"
$worker = Get-JsonPropertyValue -Object $runtime -Name "worker"
$botPid = if ($null -ne $bot) { [int](Get-JsonPropertyValue -Object $bot -Name "pid" -Default 0) } else { 0 }
$workerPid = if ($null -ne $worker) { [int](Get-JsonPropertyValue -Object $worker -Name "pid" -Default 0) } else { 0 }
if ($botPid -le 0) {
Fail-IdunnHealth -State "failed" -Message "VoidBot runtime state does not contain a live bot PID."
}
if ($workerPid -le 0) {
Fail-IdunnHealth -State "failed" -Message "VoidBot runtime state does not contain a live worker PID."
}
$botProcess = Get-ProcessOrFail -Role "bot" -ProcessId $botPid
$workerProcess = Get-ProcessOrFail -Role "worker" -ProcessId $workerPid
$orchestrator = Read-JsonFile -Path $orchestratorPath
if ($null -eq $orchestrator -or $null -eq $orchestrator.organs) {
Fail-IdunnHealth -State "failed" -Message "VoidBot orchestrator state is missing at $orchestratorPath."
}
$surfaceState = $orchestrator.organs.PSObject.Properties["voidbot-swarm-surface"]
if ($null -eq $surfaceState) {
Fail-IdunnHealth -State "failed" -Message "VoidBot orchestrator does not report the voidbot-swarm-surface organ."
}
$surfaceState = $surfaceState.Value
if ($surfaceState.lastStatus -ne "ok") {
Fail-IdunnHealth -State "degraded" -Message "VoidBot swarm surface organ lastStatus is '$($surfaceState.lastStatus)', expected 'ok'."
}
if ([string]::IsNullOrWhiteSpace($surfaceState.lastFinishedAt)) {
Fail-IdunnHealth -State "failed" -Message "VoidBot swarm surface organ has no lastFinishedAt timestamp."
}
$surfaceFinishedAt = [DateTimeOffset]::Parse($surfaceState.lastFinishedAt).ToUniversalTime()
$surfaceAgeSeconds = ([DateTimeOffset]::UtcNow - $surfaceFinishedAt).TotalSeconds
if ($surfaceAgeSeconds -gt $MaxOrchestratorAgeSeconds) {
Fail-IdunnHealth -State "failed" -Message "VoidBot swarm surface organ is stale ($([math]::Round($surfaceAgeSeconds))s old)."
}
$orchestratorFile = Get-Item -LiteralPath $orchestratorPath -ErrorAction SilentlyContinue
if ($null -eq $orchestratorFile) {
Fail-IdunnHealth -State "failed" -Message "VoidBot orchestrator state file is missing at $orchestratorPath."
}
$orchestratorAgeSeconds = ([DateTimeOffset]::UtcNow - [DateTimeOffset]$orchestratorFile.LastWriteTimeUtc).TotalSeconds
if ($orchestratorAgeSeconds -gt $MaxOrchestratorAgeSeconds) {
Fail-IdunnHealth -State "failed" -Message "VoidBot orchestrator state file is stale ($([math]::Round($orchestratorAgeSeconds))s old)."
}
$cultMeshStore = Get-Item -LiteralPath $cultMeshStorePath -ErrorAction SilentlyContinue
if ($null -eq $cultMeshStore) {
Fail-IdunnHealth -State "failed" -Message "VoidBot CultMesh witness store is missing at $cultMeshStorePath."
}
if ($cultMeshStore.Length -le 0) {
Fail-IdunnHealth -State "failed" -Message "VoidBot CultMesh witness store at $cultMeshStorePath is empty."
}
$cultMeshStoreAgeSeconds = ([DateTimeOffset]::UtcNow - [DateTimeOffset]$cultMeshStore.LastWriteTimeUtc).TotalSeconds
if ($cultMeshStoreAgeSeconds -gt $MaxCultMeshStoreAgeSeconds) {
Fail-IdunnHealth -State "failed" -Message "VoidBot CultMesh witness store is stale ($([math]::Round($cultMeshStoreAgeSeconds))s old)."
}
Assert-StoreContains -StorePath $cultMeshStorePath -Tokens @(
"voidbot.provider_advertisement_catalog.v0",
"idunn.command_boundary.v1",
"idunn.daemon_transport_profile.v1",
"voidbot.providers",
"voidbot.discord",
"voidbot.reddit",
"voidbot.archive",
"voidbot.source",
"voidbot.repo_face",
"voidbot.swarm",
"command-boundary:voidbot",
"transport:voidbot"
)
Write-Host "VoidBot runtime is ready, bot PID $($botProcess.Id) and worker PID $($workerProcess.Id) are live, and the daemon-owned CultMesh witness store is fresh."