VoidBot/scripts/run-void-moderator-rumination.ps1
2026-05-16 18:43:53 +01:00

197 lines
6 KiB
PowerShell

param(
[switch] $NoPost
)
Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"
$ProgressPreference = "SilentlyContinue"
$repoRoot = Split-Path -Parent $PSScriptRoot
Set-Location -LiteralPath $repoRoot
$stateFilePath = Join-Path $repoRoot ".voidbot\private\void-self-state.cc"
$statusDir = Join-Path $repoRoot ".voidbot\status"
$logDir = Join-Path $repoRoot ".voidbot\logs"
$statusPath = Join-Path $statusDir "moderation-rumination.json"
$summaryLogPath = Join-Path $logDir "moderation-rumination.log"
$lockPath = Join-Path $statusDir "moderation-rumination.lock"
$recentHistoryScriptPath = Join-Path $repoRoot "scripts\export-recent-discord-history.mjs"
$selfStateScriptPath = Join-Path $repoRoot "scripts\void-self-state.mjs"
$startedAtUtc = [DateTime]::UtcNow
function Write-JsonFile {
param(
[Parameter(Mandatory = $true)]
[string] $Path,
[Parameter(Mandatory = $true)]
$Data
)
$directory = Split-Path -Parent $Path
if (-not [string]::IsNullOrWhiteSpace($directory)) {
New-Item -ItemType Directory -Force -Path $directory | Out-Null
}
$json = $Data | ConvertTo-Json -Depth 12
$utf8NoBom = [System.Text.UTF8Encoding]::new($false)
[System.IO.File]::WriteAllText($Path, $json, $utf8NoBom)
}
function Append-RunLog {
param(
[Parameter(Mandatory = $true)]
[string] $Line
)
$timestamped = "[{0}] {1}" -f ([DateTime]::UtcNow.ToString("o")), $Line
Add-Content -Path $summaryLogPath -Value $timestamped -Encoding UTF8
}
function Invoke-NodeJson {
param(
[Parameter(Mandatory = $true)]
[string[]] $Arguments
)
$output = & node @Arguments
if ($LASTEXITCODE -ne 0) {
throw "Node helper failed: node $($Arguments -join ' ')"
}
if ([string]::IsNullOrWhiteSpace($output)) {
return $null
}
return $output | ConvertFrom-Json
}
function Get-TypedModerationCursor {
$script = @"
const core = require('./packages/core/dist');
core.loadVoidSelfStateTypedDocuments({ canonicalPath: process.argv[1] })
.then((state) => {
console.log(JSON.stringify({
lastReviewedMessageId: state.moderationCursor.lastReviewedMessageId ?? null,
lastReviewedTimestamp: state.moderationCursor.lastReviewedTimestamp ?? null
}));
})
.catch((error) => { console.error(error); process.exit(1); });
"@
$output = & node -e $script $stateFilePath
if ($LASTEXITCODE -ne 0) {
throw "Failed to read typed self-state cursor."
}
return $output | ConvertFrom-Json
}
function Apply-TypedOperation {
param(
[Parameter(Mandatory = $true)]
[hashtable] $Operation
)
$operationJson = $Operation | ConvertTo-Json -Compress -Depth 20
[void](Invoke-NodeJson -Arguments @(
$selfStateScriptPath,
"apply-operation",
"--canonical", $stateFilePath,
"--operation", $operationJson
))
}
trap {
$finishedAtUtc = [DateTime]::UtcNow
$failureMessage = $_.Exception.Message
Write-JsonFile -Path $statusPath -Data @{
status = "failed"
startedAt = $startedAtUtc.ToString("o")
finishedAt = $finishedAtUtc.ToString("o")
durationSeconds = [Math]::Round(($finishedAtUtc - $startedAtUtc).TotalSeconds, 2)
failureMessage = $failureMessage
noPost = [bool]$NoPost
stateFile = $stateFilePath
}
Remove-Item -Path $lockPath -Force -ErrorAction SilentlyContinue
throw
}
New-Item -ItemType Directory -Force -Path $statusDir, $logDir, (Split-Path -Parent $stateFilePath) | Out-Null
if (Test-Path $lockPath) {
$lockAge = [DateTime]::UtcNow - (Get-Item $lockPath).LastWriteTimeUtc
if ($lockAge.TotalMinutes -lt 20) {
Write-JsonFile -Path $statusPath -Data @{
status = "skipped"
reason = "lock_present"
observedAt = ([DateTime]::UtcNow.ToString("o"))
stateFile = $stateFilePath
}
return
}
Remove-Item -LiteralPath $lockPath -Force
}
Write-JsonFile -Path $lockPath -Data @{
pid = $PID
startedAt = $startedAtUtc.ToString("o")
}
if (-not (Test-Path $recentHistoryScriptPath)) {
throw "Missing recent history helper at $recentHistoryScriptPath"
}
if (-not (Test-Path $selfStateScriptPath)) {
throw "Missing typed self-state helper at $selfStateScriptPath"
}
$priorCursor = Get-TypedModerationCursor
$historyArgs = @($recentHistoryScriptPath)
if ($null -ne $priorCursor -and -not [string]::IsNullOrWhiteSpace([string]$priorCursor.lastReviewedTimestamp)) {
$historyArgs += @("--after", [string]$priorCursor.lastReviewedTimestamp, "--limit", "120")
} else {
$historyArgs += @("--hours", "6", "--limit", "120")
}
$history = Invoke-NodeJson -Arguments $historyArgs
$observedCursor = $priorCursor
$messageCount = 0
if ($null -ne $history.messages) {
$messageCount = $history.messages.Count
}
if ($messageCount -gt 0) {
$latestObservedMessage = $history.messages[$messageCount - 1]
$observedCursor = [pscustomobject]@{
lastReviewedMessageId = $latestObservedMessage.id
lastReviewedTimestamp = $latestObservedMessage.timestamp
}
if (
-not [string]::IsNullOrWhiteSpace([string]$observedCursor.lastReviewedMessageId) -and
-not [string]::IsNullOrWhiteSpace([string]$observedCursor.lastReviewedTimestamp)
) {
Apply-TypedOperation -Operation @{
operation = "record_reviewed_messages"
lastReviewedMessageId = [string]$observedCursor.lastReviewedMessageId
lastReviewedTimestamp = [string]$observedCursor.lastReviewedTimestamp
}
}
}
$finishedAtUtc = [DateTime]::UtcNow
Write-JsonFile -Path $statusPath -Data @{
status = "ok"
mode = "typed_cursor_only"
startedAt = $startedAtUtc.ToString("o")
finishedAt = $finishedAtUtc.ToString("o")
durationSeconds = [Math]::Round(($finishedAtUtc - $startedAtUtc).TotalSeconds, 2)
noPost = [bool]$NoPost
stateFile = $stateFilePath
observedMessageCount = $messageCount
previousCursor = $priorCursor
observedCursor = $observedCursor
note = "Legacy Codex rumination is offline. This runner only advances typed chronology until the typed phase machine is rebuilt."
}
Append-RunLog ("mode=typed_cursor_only messages={0} cursor={1}" -f $messageCount, ([string]$observedCursor.lastReviewedTimestamp))
Remove-Item -Path $lockPath -Force -ErrorAction SilentlyContinue