Odin/scripts/health-voidbot.ps1
2026-06-17 02:58:20 +01:00

181 lines
6.5 KiB
PowerShell

param(
[string] $VoidBotRoot = "E:\Projects\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."