diff --git a/scripts/check-voidbot-operations.ps1 b/scripts/check-voidbot-operations.ps1 index 4640d25..a4a7b20 100644 --- a/scripts/check-voidbot-operations.ps1 +++ b/scripts/check-voidbot-operations.ps1 @@ -185,6 +185,82 @@ function Get-Json { return $response | ConvertFrom-Json } +function Invoke-JsonRequestDetailed { + param( + [Parameter(Mandatory = $true)] + [string] $Url, + [string[]] $Headers = @() + ) + + $headerPath = Join-Path ([System.IO.Path]::GetTempPath()) ([System.IO.Path]::GetRandomFileName()) + + try { + $args = @( + "-sS", + "--connect-timeout", "5", + "--max-time", "20", + "-D", $headerPath, + $Url + ) + + foreach ($header in $Headers) { + $args = @("-H", $header) + $args + } + + $responseBody = & curl.exe @args + $exitCode = $LASTEXITCODE + $headerLines = if (Test-Path -LiteralPath $headerPath) { + Get-Content -LiteralPath $headerPath + } else { + @() + } + + $statusCode = $null + $statusLine = @($headerLines | Where-Object { $_ -match "^HTTP/" } | Select-Object -Last 1) + if ($statusLine.Count -gt 0 -and $statusLine[0] -match "^HTTP/\S+\s+(\d{3})") { + $statusCode = [int]$Matches[1] + } + + $parsedHeaders = @{} + foreach ($line in $headerLines) { + if ([string]::IsNullOrWhiteSpace($line) -or $line -match "^HTTP/") { + continue + } + + $separatorIndex = $line.IndexOf(":") + if ($separatorIndex -lt 1) { + continue + } + + $name = $line.Substring(0, $separatorIndex).Trim() + $value = $line.Substring($separatorIndex + 1).Trim() + $parsedHeaders[$name] = $value + } + + if ($exitCode -ne 0 -and $null -eq $statusCode) { + throw "Request failed for $Url" + } + + $json = $null + if (-not [string]::IsNullOrWhiteSpace($responseBody)) { + try { + $json = $responseBody | ConvertFrom-Json + } catch { + } + } + + return @{ + statusCode = $statusCode + headers = $parsedHeaders + body = $responseBody + json = $json + exitCode = $exitCode + } + } finally { + Remove-Item -LiteralPath $headerPath -Force -ErrorAction SilentlyContinue + } +} + function Invoke-CommandWithTimeout { param( [Parameter(Mandatory = $true)] @@ -963,10 +1039,41 @@ try { if ($config.ContainsKey("DISCORD_BOT_TOKEN") -and -not [string]::IsNullOrWhiteSpace($config["DISCORD_BOT_TOKEN"])) { Set-WatchdogStep -CurrentStep "discord_checks" -Detail "Validating Discord bot credentials." try { - $botIdentity = Get-Json -Url "https://discord.com/api/v10/users/@me" -Headers @("Authorization: Bot $($config["DISCORD_BOT_TOKEN"])") - Add-Check -Name "discord.bot_token" -Status "passed" -Detail "Discord bot token is valid for $([string]$botIdentity.username)." -Data @{ - id = [string]$botIdentity.id - username = [string]$botIdentity.username + $discordIdentityResponse = Invoke-JsonRequestDetailed -Url "https://discord.com/api/v10/users/@me" -Headers @("Authorization: Bot $($config["DISCORD_BOT_TOKEN"])") + $statusCode = $discordIdentityResponse.statusCode + + if ($statusCode -eq 200 -and $null -ne $discordIdentityResponse.json) { + $botIdentity = $discordIdentityResponse.json + Add-Check -Name "discord.bot_token" -Status "passed" -Detail "Discord bot token is valid for $([string]$botIdentity.username)." -Data @{ + id = [string]$botIdentity.id + username = [string]$botIdentity.username + } + } elseif ($statusCode -eq 429) { + $retryAfterSeconds = $null + if ($discordIdentityResponse.headers.ContainsKey("Retry-After")) { + [double]::TryParse([string]$discordIdentityResponse.headers["Retry-After"], [ref]$retryAfterSeconds) | Out-Null + } + + $detail = if ($null -ne $retryAfterSeconds) { + "Discord bot token check was rate-limited by Discord (429). Retry after about $retryAfterSeconds second(s); the token is not necessarily bad." + } else { + "Discord bot token check was rate-limited by Discord (429). The token is not necessarily bad." + } + + Add-Check -Name "discord.bot_token" -Status "warning" -Detail $detail -Data @{ + statusCode = $statusCode + retryAfterSeconds = $retryAfterSeconds + } + } else { + $message = if ($null -ne $discordIdentityResponse.json -and $discordIdentityResponse.json.PSObject.Properties.Name -contains "message") { + [string]$discordIdentityResponse.json.message + } else { + "Request failed for https://discord.com/api/v10/users/@me" + } + + Add-Check -Name "discord.bot_token" -Status "failed" -Detail "Discord bot token check failed ($statusCode): $message" -Data @{ + statusCode = $statusCode + } } } catch { Add-Check -Name "discord.bot_token" -Status "failed" -Detail "Discord bot token check failed: $($_.Exception.Message)" diff --git a/state/evidence.jsonl b/state/evidence.jsonl index c8b79b7..b130e41 100644 --- a/state/evidence.jsonl +++ b/state/evidence.jsonl @@ -64,3 +64,4 @@ {"ts":"2026-05-08T15:50:57Z","type":"architecture","status":"ok","note":"Moderation novelty now needs two bars, not one: room novelty and self novelty. The memory organ merges repeated seams into existing clusters, raises saturation and refractory pressure, tracks source-coverage pressure across repos/archive terrain, and cools familiar thoughts instead of rewarding repetition with ripeness."} {"ts":"2026-05-08T16:00:51Z","type":"architecture","status":"ok","note":"Manual moderation-state distillation showed the memory surface was overproducing near-duplicate repo-weather thoughts by storing each social flavor of the same seam as a separate semantic card, sweep, and synthesis. Periodic cleanup should merge those variants by conceptual family, rebalance recent source traces, and clear stale resonance/incubation caches so novelty pressure can regrow from a cleaner substrate."} {"ts":"2026-05-08T16:06:43Z","type":"architecture","status":"ok","note":"Quiet-room moderation no longer treats retrieval as compulsory ritual. If incubation already holds a live, grounded, low-saturation thought, Void may deepen it directly; archive dives and repo sweeps are now explicit support for hungry, stale, or oversaturated queues rather than mandatory throat-clearing before every private thought."} +{"ts":"2026-05-08T19:39:40Z","type":"implementation","status":"ok","note":"Discord bot token health checks can hit Discord API rate limits even when the token is fine. The watchdog now distinguishes HTTP 429 from actual credential failures, downgrading rate limits to a warning with retry-after data instead of reporting a dead token."}