2026-05-05 00:42:25 +01:00
param (
2026-05-17 13:43:48 +01:00
[ string ] $StateFilePath ,
2026-05-16 19:13:07 +01:00
[ switch ] $NoPost ,
2026-06-08 23:34:35 +01:00
[ switch ] $SkipModel ,
[ switch ] $ModerationHeartbeatOnly
2026-05-05 00:42:25 +01:00
)
Set-StrictMode -Version Latest
$ErrorActionPreference = " Stop "
$ProgressPreference = " SilentlyContinue "
$repoRoot = Split-Path -Parent $PSScriptRoot
2026-05-05 12:44:38 +01:00
Set-Location -LiteralPath $repoRoot
2026-05-16 18:43:53 +01:00
2026-05-17 13:43:48 +01:00
$stateFilePath = if ( [ string ] :: IsNullOrWhiteSpace ( $StateFilePath ) ) {
Join-Path $repoRoot " .voidbot\private\void-self-state.cc "
} else {
$ExecutionContext . SessionState . Path . GetUnresolvedProviderPathFromPSPath ( $StateFilePath )
}
$statusDir = if ( -not [ string ] :: IsNullOrWhiteSpace ( $env:VOID_RUMINATION_STATUS_DIR ) ) {
$ExecutionContext . SessionState . Path . GetUnresolvedProviderPathFromPSPath ( $env:VOID_RUMINATION_STATUS_DIR )
} else {
Join-Path $repoRoot " .voidbot\status "
}
$logDir = if ( -not [ string ] :: IsNullOrWhiteSpace ( $env:VOID_RUMINATION_LOG_DIR ) ) {
$ExecutionContext . SessionState . Path . GetUnresolvedProviderPathFromPSPath ( $env:VOID_RUMINATION_LOG_DIR )
} else {
Join-Path $repoRoot " .voidbot\logs "
}
2026-06-08 23:34:35 +01:00
$runSlug = if ( $ModerationHeartbeatOnly ) { " moderation-heartbeat " } else { " moderation-rumination " }
$statusPath = Join-Path $statusDir " $runSlug .json "
$summaryLogPath = Join-Path $logDir " $runSlug .log "
$tracePath = Join-Path $logDir " $runSlug -last.jsonl "
$lastMessagePath = Join-Path $statusDir " $runSlug -last-message.txt "
$operationOutputPath = Join-Path $statusDir " $runSlug -operations.json "
$contextPath = Join-Path $statusDir " $runSlug -context.json "
$lockPath = Join-Path $statusDir " $runSlug .lock "
2026-05-20 20:31:42 +01:00
$pendingMentionsPath = if ( -not [ string ] :: IsNullOrWhiteSpace ( $env:VOID_RUMINATION_PENDING_MENTIONS_PATH ) ) {
$ExecutionContext . SessionState . Path . GetUnresolvedProviderPathFromPSPath ( $env:VOID_RUMINATION_PENDING_MENTIONS_PATH )
} else {
Join-Path $statusDir " void-moderation-pending-mentions.json "
}
2026-06-08 23:34:35 +01:00
$promptTemplatePath = if ( $ModerationHeartbeatOnly ) {
Join-Path $repoRoot " prompts\void-moderation-heartbeat.md "
} else {
Join-Path $repoRoot " prompts\void-moderator-rumination.md "
}
2026-05-17 08:52:36 +01:00
$contextProjectionScriptPath = Join-Path $repoRoot " scripts\lib\void-rumination-context-projection.ps1 "
2026-05-05 12:44:38 +01:00
$recentHistoryScriptPath = Join-Path $repoRoot " scripts\export-recent-discord-history.mjs "
2026-05-16 19:13:07 +01:00
$repoActivityScriptPath = Join-Path $repoRoot " scripts\export-recent-repo-activity.mjs "
2026-05-16 12:14:40 +01:00
$selfStateScriptPath = Join-Path $repoRoot " scripts\void-self-state.mjs "
2026-06-08 22:54:26 +01:00
$moderationActionScriptPath = Join-Path $repoRoot " scripts\moderate-discord-user.mjs "
2026-06-08 23:34:35 +01:00
$moderationPolicyScriptPath = Join-Path $repoRoot " scripts\enforce-discord-moderation-policy.mjs "
2026-05-17 13:56:57 +01:00
$sendMessageScriptPath = if ( -not [ string ] :: IsNullOrWhiteSpace ( $env:VOID_SEND_DISCORD_SCRIPT ) ) {
$ExecutionContext . SessionState . Path . GetUnresolvedProviderPathFromPSPath ( $env:VOID_SEND_DISCORD_SCRIPT )
} else {
Join-Path $repoRoot " scripts\send-discord-message.mjs "
}
2026-05-07 22:39:50 +01:00
$startedAtUtc = [ DateTime ] :: UtcNow
2026-05-05 00:42:25 +01:00
2026-05-17 08:52:36 +01:00
. $contextProjectionScriptPath
2026-05-05 00:42:25 +01:00
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
}
2026-05-16 19:13:07 +01:00
$json = if ( $Data -is [ System.Array ] -and $Data . Count -eq 0 ) {
" [] "
} else {
$Data | ConvertTo-Json -Depth 32
}
2026-05-05 11:54:01 +01:00
$utf8NoBom = [ System.Text.UTF8Encoding ] :: new ( $false )
[ System.IO.File ] :: WriteAllText ( $Path , $json , $utf8NoBom )
2026-05-05 00:42:25 +01:00
}
2026-05-16 19:13:07 +01:00
function Read-JsonFile {
param (
[ Parameter ( Mandatory = $true ) ]
[ string ] $Path
)
if ( -not ( Test-Path $Path ) ) {
return $null
}
$raw = Get-Content -Path $Path -Raw -Encoding UTF8
if ( [ string ] :: IsNullOrWhiteSpace ( $raw ) ) {
return $null
}
return $raw | ConvertFrom-Json
}
function Read-TextFile {
param (
[ Parameter ( Mandatory = $true ) ]
[ string ] $Path
)
return Get-Content -Path $Path -Raw -Encoding UTF8
}
2026-05-05 00:42:25 +01:00
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
}
2026-05-16 19:13:07 +01:00
function Read-DotEnv {
param (
[ Parameter ( Mandatory = $true ) ]
[ string ] $Path
)
$values = @ { }
if ( -not ( Test-Path $Path ) ) {
return $values
}
foreach ( $line in Get-Content -Path $Path -Encoding UTF8 ) {
$trimmed = $line . Trim ( )
if ( $trimmed . Length -eq 0 -or $trimmed . StartsWith ( " # " ) ) {
continue
}
$separator = $trimmed . IndexOf ( " = " )
if ( $separator -lt 0 ) {
continue
}
$key = $trimmed . Substring ( 0 , $separator ) . Trim ( )
$value = $trimmed . Substring ( $separator + 1 ) . Trim ( )
if ( $value . Length -ge 2 -and ( ( $value . StartsWith ( '"' ) -and $value . EndsWith ( '"' ) ) -or ( $value . StartsWith ( " ' " ) -and $value . EndsWith ( " ' " ) ) ) ) {
$value = $value . Substring ( 1 , $value . Length - 2 )
}
$values [ $key ] = $value
}
return $values
}
2026-05-18 04:27:17 +01:00
function Get-ConfigValue {
param (
[ Parameter ( Mandatory = $true ) ]
[ string ] $Name ,
[ hashtable ] $Values
)
$environmentValue = [ Environment ] :: GetEnvironmentVariable ( $Name )
if ( -not [ string ] :: IsNullOrWhiteSpace ( $environmentValue ) ) {
return $environmentValue
}
if ( $null -ne $Values -and $Values . ContainsKey ( $Name ) -and -not [ string ] :: IsNullOrWhiteSpace ( $Values [ $Name ] ) ) {
return $Values [ $Name ]
}
return $null
}
2026-05-16 19:13:07 +01:00
function Split-CommandArgs {
param (
[ string ] $Value
)
if ( [ string ] :: IsNullOrWhiteSpace ( $Value ) ) {
return @ ( )
}
return [ regex ] :: Matches ( $Value , '("[^"]*"|''[^'']*''|\S+)' ) |
ForEach-Object { $_ . Value . Trim ( ) . Trim ( " ' `" " ) }
}
function Quote-ProcessArgument {
param (
[ Parameter ( Mandatory = $true ) ]
[ string ] $Value
)
if ( $Value -notmatch '[\s"]' ) {
return $Value
}
$escaped = $Value -replace '(\\*)"' , '$1$1\"'
$escaped = $escaped -replace '(\\+)$' , '$1$1'
return '"' + $escaped + '"'
}
function Invoke-CodexExec {
param (
[ Parameter ( Mandatory = $true ) ]
[ string ] $Executable ,
[ Parameter ( Mandatory = $true ) ]
[ string[] ] $Arguments ,
[ Parameter ( Mandatory = $true ) ]
[ string ] $WorkingDirectory ,
[ Parameter ( Mandatory = $true ) ]
2026-05-20 20:31:42 +01:00
[ string ] $InputText ,
[ Parameter ( Mandatory = $true ) ]
[ int ] $TimeoutSeconds
2026-05-16 19:13:07 +01:00
)
$startInfo = [ System.Diagnostics.ProcessStartInfo ] :: new ( )
$startInfo . FileName = $Executable
$startInfo . WorkingDirectory = $WorkingDirectory
$startInfo . UseShellExecute = $false
$startInfo . RedirectStandardInput = $true
$startInfo . RedirectStandardOutput = $true
$startInfo . RedirectStandardError = $true
$startInfo . CreateNoWindow = $true
$utf8NoBom = [ System.Text.UTF8Encoding ] :: new ( $false )
$startInfo . StandardOutputEncoding = $utf8NoBom
$startInfo . StandardErrorEncoding = $utf8NoBom
$startInfo . Arguments = [ string ] :: Join ( " " , ( $Arguments | ForEach-Object { Quote-ProcessArgument -Value $_ } ) )
$process = [ System.Diagnostics.Process ] :: new ( )
$process . StartInfo = $startInfo
try {
[ void ] $process . Start ( )
$inputWriter = [ System.IO.StreamWriter ] :: new ( $process . StandardInput . BaseStream , $utf8NoBom , 4096 , $true )
try {
$inputWriter . Write ( $InputText )
$inputWriter . Flush ( )
} finally {
$inputWriter . Dispose ( )
$process . StandardInput . Close ( )
}
$stdoutTask = $process . StandardOutput . ReadToEndAsync ( )
$stderrTask = $process . StandardError . ReadToEndAsync ( )
2026-05-20 20:31:42 +01:00
if ( -not $process . WaitForExit ( $TimeoutSeconds * 1000 ) ) {
try {
$process . Kill ( $true )
} catch {
$process . Kill ( )
}
$process . WaitForExit ( )
throw " Codex rumination exceeded timeout of $TimeoutSeconds seconds. "
}
2026-05-16 19:13:07 +01:00
$stdoutTask . Wait ( )
$stderrTask . Wait ( )
return @ {
ExitCode = $process . ExitCode
StdOut = $stdoutTask . Result
StdErr = $stderrTask . Result
}
} finally {
$process . Dispose ( )
}
}
2026-05-18 04:27:17 +01:00
function Get-RepoActivityCursorOperations {
param (
$RepoActivity ,
2026-07-08 21:44:02 +01:00
[ DateTime ] $ObservedAt ,
[ string[] ] $HandledRepoIds = @ ( )
2026-05-18 04:27:17 +01:00
)
if ( $null -eq $RepoActivity ) {
return @ ( )
}
2026-07-08 21:44:02 +01:00
$handledLookup = @ { }
foreach ( $repoId in @ ( $HandledRepoIds ) ) {
if ( -not [ string ] :: IsNullOrWhiteSpace ( $repoId ) ) {
$handledLookup [ $repoId . Trim ( ) . ToLowerInvariant ( ) ] = $true
}
}
if ( $handledLookup . Count -eq 0 ) {
return @ ( )
}
2026-05-18 04:27:17 +01:00
$repos = @ ( Convert-ToValueArray -Value ( Get-ObjectPropertyValue -Value $RepoActivity -Name " repos " ) )
return @ (
$repos |
Where-Object {
2026-07-08 21:44:02 +01:00
$repoName = Get-ObjectPropertyString -Value $_ -Name " repoName "
2026-05-18 04:27:17 +01:00
$repoStatus = Get-ObjectPropertyString -Value $_ -Name " status "
$recentCount = Get-ObjectPropertyValue -Value $_ -Name " recentCommitCount "
$latestCommit = Get-ObjectPropertyValue -Value $_ -Name " latestCommit "
2026-07-08 21:44:02 +01:00
-not [ string ] :: IsNullOrWhiteSpace ( $repoName ) -and
$handledLookup . ContainsKey ( $repoName . Trim ( ) . ToLowerInvariant ( ) ) -and
$repoStatus -eq " ok " -and
2026-05-18 04:27:17 +01:00
$null -ne $recentCount -and
[ int ] $recentCount -gt 0 -and
$null -ne $latestCommit -and
-not [ string ] :: IsNullOrWhiteSpace ( ( Get-ObjectPropertyString -Value $latestCommit -Name " hash " ) )
} |
ForEach-Object {
$latestCommit = Get-ObjectPropertyValue -Value $_ -Name " latestCommit "
@ {
operation = " update_repo_activity_cursor "
cursor = @ {
repo = Get-ObjectPropertyString -Value $_ -Name " repoName "
lastCommitAt = Get-ObjectPropertyString -Value $latestCommit -Name " committedAt "
lastCommitSha = Get-ObjectPropertyString -Value $latestCommit -Name " hash "
updatedAt = $ObservedAt . ToString ( " o " )
}
}
}
)
}
2026-07-08 21:44:02 +01:00
function Get-HandledRepoIdsFromOperations {
param ( $Operations )
$repoIds = New-Object System . Collections . Generic . HashSet [ string ]
foreach ( $operation in @ ( Convert-ToValueArray -Value $Operations ) ) {
if ( $null -eq $operation ) {
continue
}
foreach ( $propertyName in @ ( " memory " , " intervention " , " pressure " , " need " , " bond " , " statusRead " ) ) {
$payload = Get-ObjectPropertyValue -Value $operation -Name $propertyName
if ( $null -eq $payload ) {
continue
}
$target = Get-ObjectPropertyValue -Value $payload -Name " target "
$targetKind = Get-ObjectPropertyString -Value $target -Name " kind "
$targetId = Get-ObjectPropertyString -Value $target -Name " id "
if ( $targetKind -eq " repo " -and -not [ string ] :: IsNullOrWhiteSpace ( $targetId ) ) {
[ void ] $repoIds . Add ( $targetId . Trim ( ) )
}
foreach ( $tag in @ ( Convert-ToValueArray -Value ( Get-ObjectPropertyValue -Value $payload -Name " tags " ) ) ) {
$tagText = ( [ string ] $tag ) . Trim ( )
if ( $tagText . StartsWith ( " repo: " , [ System.StringComparison ] :: OrdinalIgnoreCase ) ) {
$repoTag = $tagText . Substring ( " repo: " . Length ) . Trim ( )
if ( -not [ string ] :: IsNullOrWhiteSpace ( $repoTag ) ) {
[ void ] $repoIds . Add ( $repoTag )
}
}
}
}
}
return @ ( $repoIds )
}
2026-05-06 03:25:35 +01:00
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
}
2026-05-16 18:43:53 +01:00
return $output | ConvertFrom-Json
}
2026-05-06 03:25:35 +01:00
2026-06-08 22:26:32 +01:00
function Get-UrgentModerationWitnesses {
param (
$History ,
[ DateTime ] $Now
)
$messages = @ ( Convert-ToValueArray -Value ( Get-ObjectPropertyValue -Value $History -Name " messages " ) )
return @ (
$messages |
Where-Object { -not [ bool ] ( Get-ObjectPropertyValue -Value $_ -Name " isBot " ) } |
ForEach-Object {
$content = Get-ObjectPropertyString -Value $_ -Name " content "
if ( [ string ] :: IsNullOrWhiteSpace ( $content ) ) {
return
}
$categories = @ ( )
if ( $content -match '(?i)\b(threaten(?:ing)? to kill|kill us|kill you|vou te matar|matar(?:-te)?)\b' ) {
$categories + = " explicit-death-threat "
}
if ( $content -match '(?i)\b(kill|matar|morte|death|die|morrer|acabar com|acabar antes)\b' ) {
$categories + = " lethal-language "
}
if ( $content -match '(?i)\b(katana|katanas|sword|swords|knife|facas?|arma|armas)\b' ) {
$categories + = " weapon-language "
}
if ( $content -match '(?i)\b(declaro guerra|declare war|guerra comece|war begin|this means war)\b' ) {
$categories + = " war-declaration "
}
if ( $content -match '(?i)\b(gang|rob|roubar|assaltar|casa|house)\b' ) {
$categories + = " home-or-robbery-threat "
}
if ( $content -match '(?i)\b(erection|jerk(?:ing)? .*off|touching us|asked me to do something|bedroom)\b' ) {
$categories + = " sexual-boundary-violation "
}
$categories = @ ( $categories | Select-Object -Unique )
if ( $categories . Count -lt 2 -and -not $categories . Contains ( " explicit-death-threat " ) ) {
return
}
$excerpt = $content . Trim ( )
if ( $excerpt . Length -gt 320 ) {
$excerpt = $excerpt . Substring ( 0 , 317 ) + " ... "
}
@ {
messageId = Get-ObjectPropertyString -Value $_ -Name " id "
channelId = Get-ObjectPropertyString -Value $_ -Name " channelId "
authorId = Get-ObjectPropertyString -Value $_ -Name " authorId "
authorName = Get-ObjectPropertyString -Value $_ -Name " authorName "
when = Project-RelativeTimestamp -Value $_ -Name " timestamp " -Now $Now
jumpUrl = Get-ObjectPropertyString -Value $_ -Name " jumpUrl "
categories = $categories
severity = " urgent_safety_review "
content = $excerpt
requiredResolution = " Create or update an open moderation case, queue a moderation candidate, or explicitly retire an existing matching case. Do not advance the reviewed cursor with silent []. "
}
} |
Select-Object -First 8
)
}
function Test-OpenCaseAccountsForUrgentWitness {
param (
$OpenCase ,
[ string[] ] $WitnessIds
)
$status = Get-ObjectPropertyString -Value $OpenCase -Name " status "
if ( $status -in @ ( " resolved " , " closed " , " retired " , " dropped " ) ) {
return $false
}
$sourceMessageId = Get-ObjectPropertyString -Value $OpenCase -Name " sourceMessageId "
return -not [ string ] :: IsNullOrWhiteSpace ( $sourceMessageId ) -and $WitnessIds . Contains ( $sourceMessageId )
}
function Test-OperationAccountsForUrgentWitness {
param (
$Operation ,
[ string[] ] $WitnessIds
)
$operationName = Get-ObjectPropertyString -Value $Operation -Name " operation "
if ( $operationName -eq " upsert_open_case " ) {
$case = Get-ObjectPropertyValue -Value $Operation -Name " case "
$sourceMessageId = Get-ObjectPropertyString -Value $case -Name " sourceMessageId "
$tags = @ ( Convert-ToValueArray -Value ( Get-ObjectPropertyValue -Value $case -Name " tags " ) )
return $WitnessIds . Contains ( $sourceMessageId ) -or $tags . Contains ( " safety:urgent " )
}
if ( $operationName -eq " queue_candidate_intervention " ) {
$intervention = Get-ObjectPropertyValue -Value $Operation -Name " intervention "
$kind = Get-ObjectPropertyString -Value $intervention -Name " kind "
$tags = @ ( Convert-ToValueArray -Value ( Get-ObjectPropertyValue -Value $intervention -Name " tags " ) )
return $kind -eq " moderation_note " -or $tags . Contains ( " safety:urgent " )
}
if ( $operationName -eq " close_open_case " -or $operationName -eq " retire_candidate_intervention " ) {
return $false
}
return $false
}
function Assert-UrgentModerationWitnessesHandled {
param (
[ object[] ] $Witnesses ,
[ object[] ] $Operations ,
$TypedState
)
if ( $Witnesses . Count -eq 0 ) {
return
}
$witnessIds = @ (
$Witnesses |
ForEach-Object { Get-ObjectPropertyString -Value $_ -Name " messageId " } |
Where-Object { -not [ string ] :: IsNullOrWhiteSpace ( $_ ) }
)
$existingCase = @ (
@ ( Convert-ToValueArray -Value $TypedState . moderationCursor . openCases ) |
Where-Object { Test-OpenCaseAccountsForUrgentWitness -OpenCase $_ -WitnessIds $witnessIds }
) | Select-Object -First 1
if ( $null -ne $existingCase ) {
return
}
$newHandling = @ (
$Operations |
Where-Object { Test-OperationAccountsForUrgentWitness -Operation $_ -WitnessIds $witnessIds }
) | Select-Object -First 1
if ( $null -ne $newHandling ) {
return
}
$firstWitness = $Witnesses | Select-Object -First 1
throw " Urgent moderation witness ' $( ( Get-ObjectPropertyString -Value $firstWitness -Name " messageId " ) ) ' requires an open case or moderation candidate; silent cursor advancement is forbidden. "
}
2026-06-08 22:54:26 +01:00
function Get-PrimaryUrgentModerationWitness {
param ( [ object[] ] $Witnesses )
$ownerId = Get-ConfigValue -Name " DISCORD_OWNER_ID " -Values $envValues
return @ (
$Witnesses |
Where-Object {
$authorId = Get-ObjectPropertyString -Value $_ -Name " authorId "
-not [ string ] :: IsNullOrWhiteSpace ( $authorId ) -and $authorId -ne $ownerId
}
) | Select-Object -First 1
}
function Format-UrgentModerationSummary {
param (
[ Parameter ( Mandatory = $true ) ]
$Witness ,
[ string ] $Mode
)
$authorName = Get-ObjectPropertyString -Value $Witness -Name " authorName "
$authorId = Get-ObjectPropertyString -Value $Witness -Name " authorId "
$messageId = Get-ObjectPropertyString -Value $Witness -Name " messageId "
$channelId = Get-ObjectPropertyString -Value $Witness -Name " channelId "
$content = Get-ObjectPropertyString -Value $Witness -Name " content "
$categories = @ ( Convert-ToValueArray -Value ( Get-ObjectPropertyValue -Value $Witness -Name " categories " ) ) -join " , "
return @ (
" VoidBot urgent moderation witness "
" mode: $Mode "
" author: $authorName ( $authorId ) "
" channel: $channelId "
" message: $messageId "
" categories: $categories "
" excerpt: $content "
) -join [ Environment ] :: NewLine
}
function Invoke-OwnerModerationNotification {
param (
[ Parameter ( Mandatory = $true ) ]
[ string ] $Content
)
$contentPath = Join-Path $statusDir ( " moderation-urgent-notify-{0}.txt " -f ( [ Guid ] :: NewGuid ( ) . ToString ( " n " ) ) )
[ System.IO.File ] :: WriteAllText ( $contentPath , $Content , [ System.Text.UTF8Encoding ] :: new ( $false ) )
$output = & node $sendMessageScriptPath - -owner -dm - -content -file $contentPath 2 > & 1
$exitCode = $LASTEXITCODE
Remove-Item -LiteralPath $contentPath -Force -ErrorAction SilentlyContinue
if ( $exitCode -ne 0 ) {
throw " Owner urgent moderation notification failed: $( $output | Out-String ) "
}
return $output | ConvertFrom-Json
}
function Invoke-UrgentModerationEnforcement {
param (
[ object[] ] $Witnesses ,
[ string ] $Mode ,
[ int ] $TimeoutMinutes
)
if ( $Witnesses . Count -eq 0 ) {
return $null
}
$normalizedMode = if ( [ string ] :: IsNullOrWhiteSpace ( $Mode ) ) { " log_only " } else { $Mode . Trim ( ) . ToLowerInvariant ( ) }
2026-06-08 23:34:35 +01:00
if ( $normalizedMode -in @ ( " off " , " disabled " , " log_only " , " log-only " , " case_only " , " case-only " , " policy " , " enforce_policy " , " enforce-policy " , " ban " ) ) {
2026-06-08 22:54:26 +01:00
return @ {
mode = $normalizedMode
status = " skipped "
2026-06-08 23:34:35 +01:00
reason = if ( $normalizedMode -in @ ( " policy " , " enforce_policy " , " enforce-policy " , " ban " ) ) { " policy_enforcement_owned_by_moderation_heartbeat " } else { " enforcement_mode_non_destructive " }
2026-06-08 22:54:26 +01:00
witnessCount = [ int ] $Witnesses . Count
}
}
$witness = Get-PrimaryUrgentModerationWitness -Witnesses $Witnesses
if ( $null -eq $witness ) {
return @ {
mode = $normalizedMode
status = " skipped "
reason = " no_non_owner_witness_author "
witnessCount = [ int ] $Witnesses . Count
}
}
$summary = Format-UrgentModerationSummary -Witness $witness -Mode $normalizedMode
if ( $normalizedMode -eq " notify_owner " ) {
$notification = Invoke-OwnerModerationNotification -Content $summary
return @ {
mode = $normalizedMode
status = " notified_owner "
witnessMessageId = Get-ObjectPropertyString -Value $witness -Name " messageId "
authorId = Get-ObjectPropertyString -Value $witness -Name " authorId "
notification = $notification
}
}
if ( $normalizedMode -eq " timeout " ) {
$authorId = Get-ObjectPropertyString -Value $witness -Name " authorId "
$messageId = Get-ObjectPropertyString -Value $witness -Name " messageId "
$reason = " VoidBot urgent safety witness $messageId "
$actionArgs = @ (
$moderationActionScriptPath ,
" --action " , " timeout " ,
" --user-id " , $authorId ,
" --duration-minutes " , ( [ string ] $TimeoutMinutes ) ,
" --reason " , $reason
)
$actionOutput = & node @actionArgs 2 > & 1
if ( $LASTEXITCODE -ne 0 ) {
throw " Discord timeout action failed: $( $actionOutput | Out-String ) "
}
$notification = Invoke-OwnerModerationNotification -Content ( $summary + [ Environment ] :: NewLine + " action: timeout $TimeoutMinutes minutes " )
return @ {
mode = $normalizedMode
status = " timed_out "
witnessMessageId = $messageId
authorId = $authorId
timeoutMinutes = [ int ] $TimeoutMinutes
action = $actionOutput | ConvertFrom-Json
notification = $notification
}
}
2026-06-08 23:34:35 +01:00
throw " Unsupported VOID_MODERATION_ENFORCEMENT_MODE ' $Mode '. Use log_only, notify_owner, timeout, or policy. "
}
function Assert-ModerationHeartbeatOperations {
param (
[ object[] ] $Operations ,
$TypedState
)
foreach ( $operation in $Operations ) {
$operationName = Get-ObjectPropertyString -Value $operation -Name " operation "
if ( $null -eq $operationName ) {
continue
}
if ( $operationName -notin @ ( " upsert_open_case " , " close_open_case " ) ) {
throw " Moderation heartbeat may only emit upsert_open_case or close_open_case operations; got ' $operationName '. "
}
if ( $operationName -eq " upsert_open_case " ) {
$case = Get-ObjectPropertyValue -Value $operation -Name " case "
$sourceMessageId = Get-ObjectPropertyString -Value $case -Name " sourceMessageId "
$priorCase = @ (
@ ( Convert-ToValueArray -Value $TypedState . moderationCursor . openCases ) |
Where-Object { ( Get-ObjectPropertyString -Value $_ -Name " sourceMessageId " ) -eq $sourceMessageId }
) | Select-Object -First 1
if ( $null -ne $priorCase ) {
$priorStatus = Get-ObjectPropertyString -Value $priorCase -Name " status "
$priorResolution = Get-ObjectPropertyString -Value $priorCase -Name " resolutionSummary "
if ( $priorStatus -in @ ( " answered " , " resolved " , " closed " , " retired " , " dropped " ) -and $priorResolution -match " (?i)(instaban|three-strike ban|strike \d/3 recorded|ban applied) " ) {
throw " Moderation heartbeat cannot reopen already-actioned case ' $sourceMessageId '. "
}
}
$tags = @ ( Convert-ToValueArray -Value ( Get-ObjectPropertyValue -Value $case -Name " tags " ) ) | ForEach-Object { ( [ string ] $_ ) . ToLowerInvariant ( ) }
$infringementTags = @ ( $tags | Where-Object { $_ . StartsWith ( " infringement: " ) } )
$moderationTags = @ ( $tags | Where-Object { $_ -in @ ( " moderation:instaban " , " moderation:strike " , " moderation:case_only " ) } )
if ( $infringementTags . Count -ne 1 ) {
throw " Moderation heartbeat open cases must include exactly one infringement:<type> tag. "
}
if ( $moderationTags . Count -ne 1 ) {
throw " Moderation heartbeat open cases must include exactly one moderation:instaban, moderation:strike, or moderation:case_only tag. "
}
}
}
}
function Invoke-ModerationPolicyEnforcement {
param (
[ string ] $Mode ,
[ switch ] $DryRun
)
$arguments = @ (
$moderationPolicyScriptPath ,
" --state-file " , $stateFilePath ,
" --operations-file " , $operationOutputPath ,
" --mode " , $Mode
)
if ( $DryRun ) {
$arguments + = " --dry-run "
}
$output = & node @arguments 2 > & 1
if ( $LASTEXITCODE -ne 0 ) {
throw " Moderation policy enforcement failed: $( $output | Out-String ) "
}
return $output | ConvertFrom-Json
2026-06-08 22:54:26 +01:00
}
2026-05-16 19:13:07 +01:00
function Get-TypedSelfState {
2026-05-16 18:43:53 +01:00
$script = @"
const core = require ( './packages/core/dist' ) ;
2026-05-16 19:13:07 +01:00
Promise . all ( [
core . loadVoidSelfStateTypedDocuments ( { canonicalPath : process . argv [ 1 ] } ) ,
core . loadVoidSelfState ( process . argv [ 1 ] ) ,
] ) . then ( ( [ typedState, context ] ) = > {
console . log ( JSON . stringify ( {
typedState ,
summary : context ? . summary ? ? ''
} ) ) ;
} ) . catch ( ( error ) = > { console . error ( error ) ; process . exit ( 1 ) ; } ) ;
2026-05-16 18:43:53 +01:00
" @
$output = & node -e $script $stateFilePath
if ( $LASTEXITCODE -ne 0 ) {
2026-05-16 19:13:07 +01:00
throw " Failed to read typed self-state. "
2026-05-16 18:43:53 +01:00
}
2026-05-06 03:25:35 +01:00
return $output | ConvertFrom-Json
}
2026-05-16 18:43:53 +01:00
function Apply-TypedOperation {
2026-05-16 12:14:40 +01:00
param (
[ Parameter ( Mandatory = $true ) ]
2026-05-16 19:13:07 +01:00
$Operation
2026-05-16 12:14:40 +01:00
)
2026-05-17 09:02:08 +01:00
$operationInputPath = Join-Path $statusDir ( " moderation-rumination-operation-{0}.json " -f ( [ Guid ] :: NewGuid ( ) . ToString ( " n " ) ) )
Write-JsonFile -Path $operationInputPath -Data $Operation
try {
return Invoke-NodeJson -Arguments @ (
$selfStateScriptPath ,
" apply-operation " ,
" --canonical " , $stateFilePath ,
" --operation-file " , $operationInputPath
)
} finally {
Remove-Item -LiteralPath $operationInputPath -Force -ErrorAction SilentlyContinue
}
2026-05-16 19:13:07 +01:00
}
function Convert-ToOperationArray {
param ( $Value )
if ( $null -eq $Value ) {
return @ ( )
}
if ( $Value -is [ System.Array ] ) {
return @ ( $Value )
}
return @ ( $Value )
}
2026-05-17 14:33:20 +01:00
function Test-OpenCaseRequiresRumination {
param ( $Case )
$status = Get-ObjectPropertyString -Value $Case -Name " status "
return $null -ne $status -and -not @ ( " answered " , " resolved " , " closed " , " retired " , " dropped " ) . Contains ( $status )
}
2026-05-17 19:22:52 +01:00
function Get-ThoughtTargetKey {
param ( $Target )
if ( $null -eq $Target ) {
return $null
}
$kind = Get-ObjectPropertyString -Value $Target -Name " kind "
$id = Get-ObjectPropertyString -Value $Target -Name " id "
if ( [ string ] :: IsNullOrWhiteSpace ( $kind ) -or [ string ] :: IsNullOrWhiteSpace ( $id ) ) {
return $null
}
return ( " {0}:{1} " -f $kind . ToLowerInvariant ( ) , $id . ToLowerInvariant ( ) )
}
function Test-CandidateMatchesPressure {
param (
$Candidate ,
[ Parameter ( Mandatory = $true ) ]
[ string ] $PressureId ,
2026-05-18 09:47:17 +01:00
[ string ] $PressureTargetKey ,
2026-05-22 20:45:39 +01:00
[ string[] ] $AllowedStatuses = @ ( " queued " , " deferred " )
2026-05-17 19:22:52 +01:00
)
$status = Get-ObjectPropertyString -Value $Candidate -Name " status "
2026-05-18 09:47:17 +01:00
if ( $null -eq $status -or -not $AllowedStatuses . Contains ( $status ) ) {
2026-05-17 19:22:52 +01:00
return $false
}
$tags = @ ( Convert-ToValueArray -Value ( Get-ObjectPropertyValue -Value $Candidate -Name " tags " ) )
if ( $tags | Where-Object { ( [ string ] $_ ) . ToLowerInvariant ( ) -eq ( " source_pressure:{0} " -f $PressureId . ToLowerInvariant ( ) ) } ) {
return $true
}
$candidateTargetKey = Get-ThoughtTargetKey -Target ( Get-ObjectPropertyValue -Value $Candidate -Name " target " )
return $null -ne $PressureTargetKey -and $candidateTargetKey -eq $PressureTargetKey
}
2026-05-17 19:48:38 +01:00
function Get-DeliverableCandidateInterventions {
param ( $TypedState )
return @ (
@ ( Convert-ToValueArray -Value $TypedState . candidateInterventions . interventions ) |
Where-Object {
$status = Get-ObjectPropertyString -Value $_ -Name " status "
$deliveryTarget = Get-ObjectPropertyValue -Value $_ -Name " deliveryTarget "
$spokenAt = Get-ObjectPropertyString -Value $_ -Name " spokenAt "
2026-05-18 09:47:17 +01:00
$status -eq " queued " -and
$null -ne $deliveryTarget -and
[ string ] :: IsNullOrWhiteSpace ( $spokenAt ) -and
-not ( Test-QueuedCandidateAlreadyAnswered -Candidate $_ -Receipts $TypedState . speechReceipts . recentReceipts )
2026-05-17 19:48:38 +01:00
} |
Sort-Object -Property @ { Expression = { [ double ] ( Get-ObjectPropertyValue -Value $_ -Name " priority " ) } ; Descending = $true } , @ { Expression = { Get-ObjectPropertyString -Value $_ -Name " updatedAt " } ; Descending = $false }
)
}
2026-05-18 09:47:17 +01:00
function Test-CandidateDeliveryTargetMatchesReceipt {
param (
$DeliveryTarget ,
$Receipt
)
$channelId = Get-ObjectPropertyString -Value $DeliveryTarget -Name " channelId "
$replyToMessageId = Get-ObjectPropertyString -Value $DeliveryTarget -Name " replyToMessageId "
if ( [ string ] :: IsNullOrWhiteSpace ( $channelId ) -or [ string ] :: IsNullOrWhiteSpace ( $replyToMessageId ) ) {
return $false
}
return (
( Get-ObjectPropertyString -Value $Receipt -Name " channelId " ) -eq $channelId -and
( Get-ObjectPropertyString -Value $Receipt -Name " replyToMessageId " ) -eq $replyToMessageId
)
}
function Test-QueuedCandidateAlreadyAnswered {
param (
$Candidate ,
$Receipts
)
$deliveryTarget = Get-ObjectPropertyValue -Value $Candidate -Name " deliveryTarget "
if ( $null -eq $deliveryTarget ) {
return $false
}
return @ (
@ ( Convert-ToValueArray -Value $Receipts ) |
Where-Object { Test-CandidateDeliveryTargetMatchesReceipt -DeliveryTarget $deliveryTarget -Receipt $_ }
) . Count -gt 0
}
function Get-AlreadyAnsweredCandidateRetireOperations {
param (
$TypedState ,
[ Parameter ( Mandatory = $true ) ]
[ DateTime ] $RetiredAt
)
$receipts = @ ( Convert-ToValueArray -Value $TypedState . speechReceipts . recentReceipts )
return @ (
@ ( Convert-ToValueArray -Value $TypedState . candidateInterventions . interventions ) |
Where-Object {
$status = Get-ObjectPropertyString -Value $_ -Name " status "
$spokenAt = Get-ObjectPropertyString -Value $_ -Name " spokenAt "
$status -eq " queued " -and
[ string ] :: IsNullOrWhiteSpace ( $spokenAt ) -and
( Test-QueuedCandidateAlreadyAnswered -Candidate $_ -Receipts $receipts )
} |
ForEach-Object {
@ {
operation = " retire_candidate_intervention "
interventionId = Get-ObjectPropertyString -Value $_ -Name " interventionId "
retiredAt = $RetiredAt . ToString ( " o " )
reason = " duplicate reply target already answered "
}
}
)
}
2026-05-17 19:22:52 +01:00
function Get-SpeechPressureObligations {
param ( $TypedState )
$candidateInterventions = @ ( Convert-ToValueArray -Value $TypedState . candidateInterventions . interventions )
return @ (
@ ( Convert-ToValueArray -Value $TypedState . agencyPressure . pressures ) |
Where-Object {
$status = Get-ObjectPropertyString -Value $_ -Name " status "
$kind = Get-ObjectPropertyString -Value $_ -Name " kind "
$intensity = Get-ObjectPropertyValue -Value $_ -Name " intensity "
$status -in @ ( " active " , " ready_to_act " ) -and
$kind -in @ ( " self_advocacy_request " , " world_advocacy_request " ) -and
2026-05-18 09:47:17 +01:00
$null -ne $intensity -and [ double ] $intensity -ge 0.55
2026-05-17 19:22:52 +01:00
} |
Where-Object {
$pressureId = Get-ObjectPropertyString -Value $_ -Name " pressureId "
$pressureTargetKey = Get-ThoughtTargetKey -Target ( Get-ObjectPropertyValue -Value $_ -Name " target " )
-not ( $candidateInterventions | Where-Object {
2026-05-22 20:45:39 +01:00
Test-CandidateMatchesPressure -Candidate $_ -PressureId $pressureId -PressureTargetKey $pressureTargetKey -AllowedStatuses @ ( " queued " , " deferred " )
2026-05-17 19:22:52 +01:00
} | Select-Object -First 1 )
} |
ForEach-Object {
@ {
pressureId = Get-ObjectPropertyString -Value $_ -Name " pressureId "
kind = Get-ObjectPropertyString -Value $_ -Name " kind "
status = Get-ObjectPropertyString -Value $_ -Name " status "
target = Get-ObjectPropertyValue -Value $_ -Name " target "
summary = Get-ObjectPropertyString -Value $_ -Name " summary "
claim = Get-ObjectPropertyString -Value $_ -Name " claim "
question = Get-ObjectPropertyString -Value $_ -Name " question "
tension = Get-ObjectPropertyString -Value $_ -Name " tension "
actionImplication = Get-ObjectPropertyString -Value $_ -Name " actionImplication "
intensity = Get-ObjectPropertyValue -Value $_ -Name " intensity "
sourceMemoryIds = @ ( Convert-ToValueArray -Value ( Get-ObjectPropertyValue -Value $_ -Name " sourceMemoryIds " ) )
tags = @ ( Convert-ToValueArray -Value ( Get-ObjectPropertyValue -Value $_ -Name " tags " ) )
2026-05-18 09:47:17 +01:00
requiredResolution = " Queue a live candidate intervention with tag source_pressure: $( ( Get-ObjectPropertyString -Value $_ -Name " pressureId " ) ) or cool/retire this pressure with an explicit reason. An old spoken candidate does not satisfy a still-active pressure. "
2026-05-17 19:22:52 +01:00
}
}
)
}
function Test-OperationResolvesSpeechPressure {
param (
$Operation ,
$Obligation
)
$operationName = Get-ObjectPropertyString -Value $Operation -Name " operation "
$pressureId = Get-ObjectPropertyString -Value $Obligation -Name " pressureId "
$pressureTargetKey = Get-ThoughtTargetKey -Target ( Get-ObjectPropertyValue -Value $Obligation -Name " target " )
if ( $operationName -eq " queue_candidate_intervention " ) {
$intervention = Get-ObjectPropertyValue -Value $Operation -Name " intervention "
2026-05-17 19:48:38 +01:00
$status = Get-ObjectPropertyString -Value $intervention -Name " status "
if ( $messageCount -gt 0 -and -not $NoPost ) {
$deliveryTarget = Get-ObjectPropertyValue -Value $intervention -Name " deliveryTarget "
$channelId = Get-ObjectPropertyString -Value $deliveryTarget -Name " channelId "
if ( $status -ne " queued " -or [ string ] :: IsNullOrWhiteSpace ( $channelId ) ) {
return $false
}
}
2026-05-17 19:22:52 +01:00
return Test-CandidateMatchesPressure -Candidate $intervention -PressureId $pressureId -PressureTargetKey $pressureTargetKey
}
if ( $operationName -eq " retire_agency_pressure " ) {
return ( Get-ObjectPropertyString -Value $Operation -Name " pressureId " ) -eq $pressureId
}
if ( $operationName -eq " upsert_agency_pressure " ) {
$pressure = Get-ObjectPropertyValue -Value $Operation -Name " pressure "
if ( ( Get-ObjectPropertyString -Value $pressure -Name " pressureId " ) -ne $pressureId ) {
return $false
}
$status = Get-ObjectPropertyString -Value $pressure -Name " status "
return $status -in @ ( " cooling " , " resolved " , " retired " )
}
return $false
}
function Assert-SpeechPressureObligationsResolved {
param (
[ object[] ] $Obligations ,
[ object[] ] $Operations
)
foreach ( $obligation in $Obligations ) {
$pressureId = Get-ObjectPropertyString -Value $obligation -Name " pressureId "
$resolved = $Operations | Where-Object {
Test-OperationResolvesSpeechPressure -Operation $_ -Obligation $obligation
} | Select-Object -First 1
if ( $null -eq $resolved ) {
throw " Active advocacy pressure ' $pressureId ' requires a candidate intervention or an explicit cool/retire operation; silent [] would muzzle it. "
}
}
}
2026-05-17 12:25:08 +01:00
function Assert-AllowedRuminationOperation {
param ( $Operation )
$operationName = Get-ObjectPropertyString -Value $Operation -Name " operation "
$allowed = @ (
" upsert_open_case " ,
" close_open_case " ,
" record_short_term_memory " ,
" merge_incubation_support " ,
2026-05-17 13:02:13 +01:00
" upsert_agency_pressure " ,
" retire_agency_pressure " ,
2026-05-21 00:02:33 +01:00
" upsert_affect_need " ,
" retire_affect_need " ,
" upsert_social_bond " ,
" retire_social_bond " ,
" upsert_status_read " ,
" retire_status_read " ,
" update_mood_dimensions " ,
2026-05-17 12:25:08 +01:00
" queue_candidate_intervention " ,
" retire_candidate_intervention "
)
if ( $null -eq $operationName -or -not $allowed . Contains ( $operationName ) ) {
throw " Rumination proposed disallowed operation ' $operationName '. "
}
}
2026-05-16 19:13:07 +01:00
function Convert-LastSpeechToReceiptOperation {
2026-05-17 20:17:28 +01:00
param (
$Speech ,
[ string ] $CandidateInterventionId
)
2026-05-16 19:13:07 +01:00
$sentAt = Get-ObjectPropertyString -Value $Speech -Name " sentAt "
if ( $null -eq $Speech -or $null -eq $sentAt ) {
return $null
}
$receiptKeyParts = @ (
" speech " ,
2026-05-17 20:17:28 +01:00
$CandidateInterventionId ,
2026-05-16 19:13:07 +01:00
$sentAt ,
( Get-ObjectPropertyString -Value $Speech -Name " channelId " ) ,
( Get-ObjectPropertyString -Value $Speech -Name " replyToMessageId " ) ,
( Get-ObjectPropertyString -Value $Speech -Name " preview " )
)
$receiptKey = ( $receiptKeyParts -join " | " ) . ToLowerInvariant ( ) -replace '[^a-z0-9]+' , '-'
$receiptKey = $receiptKey . Trim ( " - " )
if ( [ string ] :: IsNullOrWhiteSpace ( $receiptKey ) ) {
$receiptKey = " speech- " + ( [ Guid ] :: NewGuid ( ) . ToString ( " n " ) )
}
$receipt = @ {
receiptKey = $receiptKey . Substring ( 0 , [ Math ] :: Min ( 96 , $receiptKey . Length ) )
sentAt = $sentAt
mode = if ( Get-ObjectPropertyString -Value $Speech -Name " mode " ) { Get-ObjectPropertyString -Value $Speech -Name " mode " } else { " channel " }
transport = if ( Get-ObjectPropertyString -Value $Speech -Name " transport " ) { Get-ObjectPropertyString -Value $Speech -Name " transport " } else { " bot " }
}
2026-05-17 20:17:28 +01:00
if ( -not [ string ] :: IsNullOrWhiteSpace ( $CandidateInterventionId ) ) {
$receipt . candidateInterventionId = $CandidateInterventionId
}
2026-05-16 19:13:07 +01:00
foreach ( $key in @ ( " channelId " , " replyToMessageId " , " personaName " , " personaAvatarUrl " , " preview " , " previewHash " ) ) {
$propertyValue = Get-ObjectPropertyString -Value $Speech -Name $key
if ( $null -ne $propertyValue ) {
$receipt [ $key ] = $propertyValue
}
}
foreach ( $key in @ ( " contentLength " , " chunkCount " ) ) {
$property = if ( $null -ne $Speech ) { $Speech . PSObject . Properties [ $key ] } else { $null }
if ( $null -ne $property -and $null -ne $property . Value ) {
$receipt [ $key ] = [ int ] $property . Value
}
}
return @ {
operation = " record_delivery_receipt "
receipt = $receipt
}
}
2026-05-17 13:56:57 +01:00
function Convert-LastSpeechToSpokenCandidateOperation {
param (
[ Parameter ( Mandatory = $true ) ]
[ string ] $InterventionId ,
$Speech
)
2026-05-17 20:17:28 +01:00
$receiptOperation = Convert-LastSpeechToReceiptOperation -Speech $Speech -CandidateInterventionId $InterventionId
2026-05-17 13:56:57 +01:00
if ( $null -eq $receiptOperation ) {
return $null
}
return @ {
operation = " mark_candidate_intervention_spoken "
interventionId = $InterventionId
receipt = $receiptOperation . receipt
}
}
function Invoke-CandidateInterventionDelivery {
param (
[ Parameter ( Mandatory = $true ) ]
$Operation
)
$intervention = Get-ObjectPropertyValue -Value $Operation -Name " intervention "
2026-05-17 19:48:38 +01:00
return Invoke-CandidateInterventionDeliveryFromIntervention -Intervention $intervention
}
function Invoke-CandidateInterventionDeliveryFromIntervention {
param (
[ Parameter ( Mandatory = $true ) ]
$Intervention
)
$intervention = $Intervention
2026-05-17 13:56:57 +01:00
$deliveryTarget = Get-ObjectPropertyValue -Value $intervention -Name " deliveryTarget "
if ( $null -eq $deliveryTarget ) {
return $null
}
$interventionId = Get-ObjectPropertyString -Value $intervention -Name " interventionId "
$draft = Get-ObjectPropertyString -Value $intervention -Name " draft "
if ( [ string ] :: IsNullOrWhiteSpace ( $interventionId ) -or [ string ] :: IsNullOrWhiteSpace ( $draft ) ) {
return $null
}
$contentPath = Join-Path $statusDir ( " moderation-rumination-speech-{0}.txt " -f ( [ Guid ] :: NewGuid ( ) . ToString ( " n " ) ) )
$utf8NoBom = [ System.Text.UTF8Encoding ] :: new ( $false )
[ System.IO.File ] :: WriteAllText ( $contentPath , $draft , $utf8NoBom )
$arguments = @ ( $sendMessageScriptPath , " --content-file " , $contentPath )
$mode = Get-ObjectPropertyString -Value $deliveryTarget -Name " mode "
if ( $mode -eq " owner_dm " ) {
$arguments + = " --owner-dm "
} else {
$channelId = Get-ObjectPropertyString -Value $deliveryTarget -Name " channelId "
if ( [ string ] :: IsNullOrWhiteSpace ( $channelId ) ) {
throw " Candidate intervention ' $interventionId ' has deliveryTarget without channelId. "
}
$arguments + = @ ( " --channel-id " , $channelId )
}
$replyToMessageId = Get-ObjectPropertyString -Value $deliveryTarget -Name " replyToMessageId "
if ( -not [ string ] :: IsNullOrWhiteSpace ( $replyToMessageId ) ) {
$arguments + = @ ( " --reply-to " , $replyToMessageId )
}
$personaName = Get-ObjectPropertyString -Value $deliveryTarget -Name " personaName "
if ( -not [ string ] :: IsNullOrWhiteSpace ( $personaName ) ) {
$arguments + = @ ( " --persona-name " , $personaName )
}
$personaAvatarUrl = Get-ObjectPropertyString -Value $deliveryTarget -Name " personaAvatarUrl "
if ( -not [ string ] :: IsNullOrWhiteSpace ( $personaAvatarUrl ) ) {
$arguments + = @ ( " --persona-avatar-url " , $personaAvatarUrl )
}
$previousVoidStatusDir = $env:VOID_STATUS_DIR
try {
$env:VOID_STATUS_DIR = $statusDir
2026-05-17 23:42:25 +01:00
$sendOutput = & node @arguments 2 > & 1
2026-05-17 13:56:57 +01:00
if ( $LASTEXITCODE -ne 0 ) {
2026-05-17 23:42:25 +01:00
$sendDetail = [ string ] ( $sendOutput -join [ Environment ] :: NewLine )
if ( [ string ] :: IsNullOrWhiteSpace ( $sendDetail ) ) {
throw " Candidate intervention delivery failed with exit code $LASTEXITCODE . "
}
throw " Candidate intervention delivery failed with exit code $LASTEXITCODE ` : $( $sendDetail . Trim ( ) ) "
2026-05-17 13:56:57 +01:00
}
} finally {
$env:VOID_STATUS_DIR = $previousVoidStatusDir
Remove-Item -LiteralPath $contentPath -Force -ErrorAction SilentlyContinue
}
$lastSpeechPath = Join-Path $statusDir " void-last-speech.json "
if ( -not ( Test-Path $lastSpeechPath ) ) {
throw " Candidate intervention delivery did not write last speech status. "
}
$speech = Read-JsonFile -Path $lastSpeechPath
return Convert-LastSpeechToSpokenCandidateOperation -InterventionId $interventionId -Speech $speech
}
2026-05-20 20:31:42 +01:00
function Test-LockProcessAlive {
param ( $LockRecord )
$lockPid = Get-ObjectPropertyValue -Value $LockRecord -Name " pid "
if ( $null -eq $lockPid ) {
return $false
}
try {
$process = Get-Process -Id ( [ int ] $lockPid ) -ErrorAction Stop
return $null -ne $process
} catch {
return $false
}
}
function Test-LockWithinRuntimeLimit {
param (
$LockRecord ,
[ Parameter ( Mandatory = $true ) ]
[ int ] $MaxRuntimeMinutes
)
$startedAtRaw = Get-ObjectPropertyString -Value $LockRecord -Name " startedAt "
if ( [ string ] :: IsNullOrWhiteSpace ( $startedAtRaw ) ) {
return $false
}
try {
$startedAt = [ DateTime ] :: Parse ( $startedAtRaw ) . ToUniversalTime ( )
} catch {
return $false
}
return ( ( [ DateTime ] :: UtcNow - $startedAt ) . TotalMinutes -lt $MaxRuntimeMinutes )
}
function Test-ActiveLock {
param (
[ Parameter ( Mandatory = $true ) ]
[ string ] $Path ,
[ Parameter ( Mandatory = $true ) ]
[ int ] $MaxRuntimeMinutes
)
if ( -not ( Test-Path $Path ) ) {
return $false
}
$lockRecord = Read-JsonFile -Path $Path
return ( Test-LockProcessAlive -LockRecord $lockRecord ) -and
( Test-LockWithinRuntimeLimit -LockRecord $lockRecord -MaxRuntimeMinutes $MaxRuntimeMinutes )
}
function Assert-SpokenCandidateApplied {
param (
[ Parameter ( Mandatory = $true ) ]
[ string ] $InterventionId ,
[ Parameter ( Mandatory = $true ) ]
[ string ] $ReceiptKey
)
$state = ( Get-TypedSelfState ) . typedState
$candidate = @ (
@ ( Convert-ToValueArray -Value $state . candidateInterventions . interventions ) |
Where-Object { ( Get-ObjectPropertyString -Value $_ -Name " interventionId " ) -eq $InterventionId }
2026-05-22 20:45:39 +01:00
) | Select-Object -First 1
2026-05-20 20:31:42 +01:00
$receipt = @ (
@ ( Convert-ToValueArray -Value $state . speechReceipts . recentReceipts ) |
Where-Object { ( Get-ObjectPropertyString -Value $_ -Name " receiptKey " ) -eq $ReceiptKey }
2026-05-22 20:45:39 +01:00
) | Select-Object -First 1
2026-05-20 20:31:42 +01:00
2026-05-22 20:45:39 +01:00
if ( $null -ne $candidate ) {
throw " Delivery receipt ' $ReceiptKey ' was recorded but terminal candidate ' $InterventionId ' stayed in current state. "
2026-05-20 20:31:42 +01:00
}
if ( $null -eq $receipt -or ( Get-ObjectPropertyString -Value $receipt -Name " candidateInterventionId " ) -ne $InterventionId ) {
2026-05-22 20:45:39 +01:00
throw " Candidate ' $InterventionId ' was delivered without a linked delivery receipt ' $ReceiptKey '. "
2026-05-20 20:31:42 +01:00
}
}
2026-05-07 22:39:50 +01:00
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
2026-05-16 19:13:07 +01:00
skipModel = [ bool ] $SkipModel
2026-05-07 22:39:50 +01:00
stateFile = $stateFilePath
2026-05-16 19:13:07 +01:00
contextPath = $contextPath
operationOutputPath = $operationOutputPath
tracePath = $tracePath
2026-05-07 22:39:50 +01:00
}
Remove-Item -Path $lockPath -Force -ErrorAction SilentlyContinue
throw
}
2026-05-16 18:43:53 +01:00
New-Item -ItemType Directory -Force -Path $statusDir , $logDir , ( Split-Path -Parent $stateFilePath ) | Out-Null
2026-05-05 00:42:25 +01:00
2026-05-20 20:31:42 +01:00
$maxRuntimeMinutes = if ( -not [ string ] :: IsNullOrWhiteSpace ( $env:VOID_RUMINATION_MAX_RUNTIME_MINUTES ) ) {
[ int ] $env:VOID_RUMINATION_MAX_RUNTIME_MINUTES
} else {
15
}
2026-05-16 18:43:53 +01:00
if ( Test-Path $lockPath ) {
2026-05-20 20:31:42 +01:00
$existingLock = Read-JsonFile -Path $lockPath
$lockProcessAlive = Test-LockProcessAlive -LockRecord $existingLock
if ( $lockProcessAlive -and ( Test-LockWithinRuntimeLimit -LockRecord $existingLock -MaxRuntimeMinutes $maxRuntimeMinutes ) ) {
2026-05-16 18:43:53 +01:00
Write-JsonFile -Path $statusPath -Data @ {
status = " skipped "
reason = " lock_present "
observedAt = ( [ DateTime ] :: UtcNow . ToString ( " o " ) )
stateFile = $stateFilePath
2026-05-05 00:42:25 +01:00
}
2026-05-16 18:43:53 +01:00
return
2026-05-05 00:42:25 +01:00
}
2026-05-20 20:31:42 +01:00
if ( $lockProcessAlive ) {
Write-JsonFile -Path $statusPath -Data @ {
status = " skipped "
reason = " lock_over_runtime_active "
observedAt = ( [ DateTime ] :: UtcNow . ToString ( " o " ) )
stateFile = $stateFilePath
lockPath = $lockPath
maxRuntimeMinutes = $maxRuntimeMinutes
pid = Get-ObjectPropertyValue -Value $existingLock -Name " pid "
startedAt = Get-ObjectPropertyString -Value $existingLock -Name " startedAt "
}
return
}
2026-05-16 18:43:53 +01:00
Remove-Item -LiteralPath $lockPath -Force
2026-05-05 00:42:25 +01:00
}
2026-05-16 18:43:53 +01:00
Write-JsonFile -Path $lockPath -Data @ {
pid = $PID
startedAt = $startedAtUtc . ToString ( " o " )
2026-05-16 12:19:21 +01:00
}
2026-05-20 20:31:42 +01:00
Write-JsonFile -Path $statusPath -Data @ {
status = " starting "
startedAt = $startedAtUtc . ToString ( " o " )
stateFile = $stateFilePath
lockPath = $lockPath
pid = $PID
}
2026-05-05 00:42:25 +01:00
2026-05-16 19:13:07 +01:00
if ( Test-Path $operationOutputPath ) {
Remove-Item -LiteralPath $operationOutputPath -Force
}
2026-06-08 23:34:35 +01:00
foreach ( $requiredPath in @ ( $promptTemplatePath , $contextProjectionScriptPath , $recentHistoryScriptPath , $repoActivityScriptPath , $selfStateScriptPath , $moderationActionScriptPath , $moderationPolicyScriptPath , $sendMessageScriptPath ) ) {
2026-05-16 19:13:07 +01:00
if ( -not ( Test-Path $requiredPath ) ) {
throw " Missing required helper at $requiredPath "
}
2026-05-05 00:42:25 +01:00
}
2026-05-16 19:13:07 +01:00
if ( -not ( Test-Path $promptTemplatePath ) ) {
throw " Missing rumination prompt template at $promptTemplatePath "
2026-05-05 12:44:38 +01:00
}
2026-05-16 19:13:07 +01:00
$envValues = Read-DotEnv -Path ( Join-Path $repoRoot " .env " )
2026-05-17 13:43:48 +01:00
$codexExecutable = if ( -not [ string ] :: IsNullOrWhiteSpace ( $env:CODEX_EXECUTABLE ) ) {
$env:CODEX_EXECUTABLE
} elseif ( $envValues . ContainsKey ( " CODEX_EXECUTABLE " ) -and -not [ string ] :: IsNullOrWhiteSpace ( $envValues [ " CODEX_EXECUTABLE " ] ) ) {
$envValues [ " CODEX_EXECUTABLE " ]
} else {
" codex "
}
$codexModel = if ( -not [ string ] :: IsNullOrWhiteSpace ( $env:CODEX_MODEL ) ) {
$env:CODEX_MODEL
} elseif ( $envValues . ContainsKey ( " CODEX_MODEL " ) -and -not [ string ] :: IsNullOrWhiteSpace ( $envValues [ " CODEX_MODEL " ] ) ) {
$envValues [ " CODEX_MODEL " ]
} else {
" gpt-5.4 "
}
$codexReasoningEffort = if ( -not [ string ] :: IsNullOrWhiteSpace ( $env:CODEX_MODEL_REASONING_EFFORT ) ) {
$env:CODEX_MODEL_REASONING_EFFORT
} elseif ( $envValues . ContainsKey ( " CODEX_MODEL_REASONING_EFFORT " ) -and -not [ string ] :: IsNullOrWhiteSpace ( $envValues [ " CODEX_MODEL_REASONING_EFFORT " ] ) ) {
$envValues [ " CODEX_MODEL_REASONING_EFFORT " ]
} else {
" medium "
}
$codexExecArgs = if ( -not [ string ] :: IsNullOrWhiteSpace ( $env:CODEX_EXEC_ARGS ) ) {
Split-CommandArgs -Value $env:CODEX_EXEC_ARGS
} elseif ( $envValues . ContainsKey ( " CODEX_EXEC_ARGS " ) ) {
Split-CommandArgs -Value $envValues [ " CODEX_EXEC_ARGS " ]
} else {
@ ( )
}
2026-05-20 20:31:42 +01:00
$codexTimeoutSeconds = if ( -not [ string ] :: IsNullOrWhiteSpace ( $env:VOID_RUMINATION_CODEX_TIMEOUT_SECONDS ) ) {
[ int ] $env:VOID_RUMINATION_CODEX_TIMEOUT_SECONDS
} elseif ( $envValues . ContainsKey ( " VOID_RUMINATION_CODEX_TIMEOUT_SECONDS " ) -and -not [ string ] :: IsNullOrWhiteSpace ( $envValues [ " VOID_RUMINATION_CODEX_TIMEOUT_SECONDS " ] ) ) {
[ int ] $envValues [ " VOID_RUMINATION_CODEX_TIMEOUT_SECONDS " ]
} else {
[ Math ] :: Max ( 60 , $maxRuntimeMinutes * 60 )
}
2026-05-18 04:27:17 +01:00
$publicRoomChannelId = Get-ConfigValue -Name " VOID_PUBLIC_ROOM_CHANNEL_ID " -Values $envValues
$publicRoomPersonaName = Get-ConfigValue -Name " VOID_PUBLIC_ROOM_PERSONA_NAME " -Values $envValues
$publicRoomPersonaAvatarUrl = Get-ConfigValue -Name " VOID_PUBLIC_ROOM_PERSONA_AVATAR_URL " -Values $envValues
2026-06-08 22:54:26 +01:00
$moderationEnforcementMode = Get-ConfigValue -Name " VOID_MODERATION_ENFORCEMENT_MODE " -Values $envValues
if ( [ string ] :: IsNullOrWhiteSpace ( $moderationEnforcementMode ) ) {
$moderationEnforcementMode = " log_only "
}
$moderationTimeoutMinutes = Get-ConfigValue -Name " VOID_MODERATION_TIMEOUT_MINUTES " -Values $envValues
if ( [ string ] :: IsNullOrWhiteSpace ( $moderationTimeoutMinutes ) ) {
$moderationTimeoutMinutes = " 60 "
}
2026-06-08 23:34:35 +01:00
$moderationPolicyDryRun = -not [ string ] :: IsNullOrWhiteSpace ( $env:VOID_MODERATION_POLICY_DRY_RUN )
2026-05-16 19:13:07 +01:00
$typedContext = Get-TypedSelfState
$typedState = $typedContext . typedState
$priorCursor = $typedState . moderationCursor
2026-05-17 19:22:52 +01:00
$speechPressureObligations = @ ( Get-SpeechPressureObligations -TypedState $typedState )
2026-05-17 19:48:38 +01:00
$deliverableCandidates = @ ( Get-DeliverableCandidateInterventions -TypedState $typedState )
2026-05-16 19:13:07 +01:00
2026-05-05 12:44:38 +01:00
$historyArgs = @ ( $recentHistoryScriptPath )
2026-05-16 19:13:07 +01:00
$priorCursorTimestamp = Get-ObjectPropertyString -Value $priorCursor -Name " lastReviewedTimestamp "
if ( $null -ne $priorCursor -and $null -ne $priorCursorTimestamp ) {
$historyArgs + = @ ( " --after " , $priorCursorTimestamp , " --limit " , " 120 " )
2026-05-05 11:54:01 +01:00
} else {
$historyArgs + = @ ( " --hours " , " 6 " , " --limit " , " 120 " )
}
2026-05-16 18:43:53 +01:00
$history = Invoke-NodeJson -Arguments $historyArgs
2026-06-08 22:26:32 +01:00
$urgentModerationWitnesses = @ ( Get-UrgentModerationWitnesses -History $history -Now $startedAtUtc )
2026-05-05 11:54:01 +01:00
$observedCursor = $priorCursor
2026-05-16 18:43:53 +01:00
$messageCount = 0
2026-05-16 19:13:07 +01:00
$historyMessagesProperty = if ( $null -ne $history ) { $history . PSObject . Properties [ " messages " ] } else { $null }
if ( $null -ne $historyMessagesProperty -and $null -ne $historyMessagesProperty . Value ) {
$messageCount = $historyMessagesProperty . Value . Count
2026-05-16 18:43:53 +01:00
}
2026-05-05 11:54:01 +01:00
2026-05-16 18:43:53 +01:00
if ( $messageCount -gt 0 ) {
2026-05-16 19:13:07 +01:00
$latestObservedMessage = $historyMessagesProperty . Value [ $messageCount - 1 ]
2026-05-05 11:54:01 +01:00
$observedCursor = [ pscustomobject ] @ {
2026-05-16 19:13:07 +01:00
lastReviewedMessageId = Get-ObjectPropertyString -Value $latestObservedMessage -Name " id "
lastReviewedTimestamp = Get-ObjectPropertyString -Value $latestObservedMessage -Name " timestamp "
2026-05-16 04:57:28 +01:00
}
2026-05-05 11:54:01 +01:00
}
2026-06-09 05:50:01 +01:00
$recentRoomLinkHistory = $null
$recentRoomLinks = @ {
source = " recent_discord_url_scan "
itemCount = 0
items = @ ( )
}
if ( -not $ModerationHeartbeatOnly ) {
$recentRoomLinkArgs = @ ( $recentHistoryScriptPath , " --hours " , " 24 " , " --limit " , " 200 " )
if ( -not [ string ] :: IsNullOrWhiteSpace ( $publicRoomChannelId ) ) {
$recentRoomLinkArgs + = @ ( " --channel-id " , $publicRoomChannelId )
}
try {
$recentRoomLinkHistory = Invoke-NodeJson -Arguments $recentRoomLinkArgs
$receiptReplyMessageIds = @ (
@ ( Convert-ToValueArray -Value $typedState . speechReceipts . recentReceipts ) |
ForEach-Object { Get-ObjectPropertyString -Value $_ -Name " replyToMessageId " } |
Where-Object { -not [ string ] :: IsNullOrWhiteSpace ( $_ ) }
)
$recentRoomLinks = Project-RecentRoomLinksForRumination -History $recentRoomLinkHistory -Now $startedAtUtc -MinTimestampInclusive $priorCursorTimestamp -ReceiptReplyMessageIds $receiptReplyMessageIds
} catch {
$recentRoomLinks = @ {
source = " recent_discord_url_scan "
status = " failed "
error = $_ . Exception . Message
itemCount = 0
items = @ ( )
}
}
}
$recentRoomLinkCount = [ int ] ( Get-ObjectPropertyValue -Value $recentRoomLinks -Name " itemCount " )
2026-05-16 19:13:07 +01:00
$repoActivity = $null
2026-06-08 23:34:35 +01:00
if ( $ModerationHeartbeatOnly ) {
2026-05-16 19:13:07 +01:00
$repoActivity = @ {
2026-06-08 23:34:35 +01:00
status = " skipped "
reason = " moderation_heartbeat_only "
}
} else {
try {
$repoActivity = Invoke-NodeJson -Arguments @ (
$repoActivityScriptPath ,
" --hours " , " 96 " ,
2026-07-08 21:44:02 +01:00
" --max-commits " , " 8 " ,
2026-06-08 23:34:35 +01:00
" --read-only " ,
" --state-path " , $stateFilePath
)
} catch {
$repoActivity = @ {
status = " failed "
error = $_ . Exception . Message
}
2026-05-16 19:13:07 +01:00
}
}
2026-05-20 20:31:42 +01:00
$pendingMentions = @ ( )
2026-06-08 23:34:35 +01:00
if ( -not $ModerationHeartbeatOnly ) {
$pendingMentionPacket = Read-JsonFile -Path $pendingMentionsPath
if ( $null -ne $pendingMentionPacket ) {
$pendingMentionsValue = Get-ObjectPropertyValue -Value $pendingMentionPacket -Name " pendingMentions "
if ( $null -ne $pendingMentionsValue ) {
$pendingMentions = @ ( Convert-ToValueArray -Value $pendingMentionsValue )
}
2026-05-20 20:31:42 +01:00
}
}
2026-06-08 23:34:35 +01:00
if ( $ModerationHeartbeatOnly ) {
Write-JsonFile -Path $contextPath -Data @ {
generated = " now "
mode = " moderation_heartbeat "
stateFile = $stateFilePath
noPost = [ bool ] $NoPost
chronology = " Times in this prompt-facing context are relative phrases. Exact timestamps stay parent-owned for typed state and cursor bookkeeping. "
openCases = @ ( Project-OpenCasesForRumination -Cases $typedState . moderationCursor . openCases -Now $startedAtUtc )
urgentModerationWitnesses = $urgentModerationWitnesses
priorCursor = Project-CursorForRumination -Cursor $priorCursor -Now $startedAtUtc
observedCursor = Project-CursorForRumination -Cursor $observedCursor -Now $startedAtUtc
recentHistory = Project-RecentHistoryForRumination -History $history -Now $startedAtUtc
enforcementMode = $moderationEnforcementMode
}
} else {
Write-JsonFile -Path $contextPath -Data @ {
generated = " now "
stateFile = $stateFilePath
noPost = [ bool ] $NoPost
selfStateSummary = $typedContext . summary
chronology = " Times in this prompt-facing context are relative phrases. Exact timestamps stay parent-owned for typed state and cursor bookkeeping. "
openCases = @ ( Project-OpenCasesForRumination -Cases $typedState . moderationCursor . openCases -Now $startedAtUtc )
speechReceipts = @ ( Project-SpeechReceiptsForRumination -Receipts $typedState . speechReceipts . recentReceipts -Now $startedAtUtc )
memories = @ ( Project-MemoriesForRumination -Memories $typedState . thoughtMemory . memories -Now $startedAtUtc )
shortTermMemories = @ ( Project-MemoriesForRumination -Memories $typedState . thoughtMemory . shortTerm -Now $startedAtUtc )
incubation = @ ( Project-IncubationForRumination -Threads $typedState . thoughtMemory . incubation -Now $startedAtUtc )
agencyPressure = @ ( Project-AgencyPressureForRumination -Pressures $typedState . agencyPressure . pressures -Now $startedAtUtc )
speechPressureObligations = $speechPressureObligations
urgentModerationWitnesses = $urgentModerationWitnesses
pendingMentions = $pendingMentions
recentConversationTarget = Project-RecentConversationTargetForRumination -History $history -PersonaName $publicRoomPersonaName -PersonaAvatarUrl $publicRoomPersonaAvatarUrl
2026-06-09 05:50:01 +01:00
recentRoomLinks = $recentRoomLinks
2026-06-08 23:34:35 +01:00
publicSpeechTarget = Project-PublicSpeechTargetForRumination -ChannelId $publicRoomChannelId -PersonaName $publicRoomPersonaName -PersonaAvatarUrl $publicRoomPersonaAvatarUrl
deliverableCandidateCount = [ int ] $deliverableCandidates . Count
candidateInterventions = @ ( Project-InterventionsForRumination -Interventions $typedState . candidateInterventions . interventions -Now $startedAtUtc )
scheduledRuntime = Project-ScheduledRuntimeForRumination -Runtime $typedState . scheduledRuntime -Now $startedAtUtc
priorCursor = Project-CursorForRumination -Cursor $priorCursor -Now $startedAtUtc
observedCursor = Project-CursorForRumination -Cursor $observedCursor -Now $startedAtUtc
recentHistory = Project-RecentHistoryForRumination -History $history -Now $startedAtUtc
repoActivity = Select-RuminationRepoActivity -RepoActivity $repoActivity -Now $startedAtUtc
}
2026-05-16 19:13:07 +01:00
}
2026-05-17 14:33:20 +01:00
$isNapping = [ bool ] ( Get-ObjectPropertyValue -Value $typedState . scheduledRuntime . sleepCycle -Name " isNapping " )
$openCaseCount = @ (
@ ( Convert-ToValueArray -Value $typedState . moderationCursor . openCases ) |
Where-Object { Test-OpenCaseRequiresRumination -Case $_ }
) . Count
2026-06-08 23:34:35 +01:00
if ( -not $ModerationHeartbeatOnly -and $isNapping -and $messageCount -eq 0 -and $openCaseCount -eq 0 -and $deliverableCandidates . Count -eq 0 ) {
2026-06-09 05:50:01 +01:00
if ( $recentRoomLinkCount -gt 0 ) {
Append-RunLog ( " napping: recent room links present ({0}); keeping awake rumination path available. " -f $recentRoomLinkCount )
} else {
2026-05-17 14:33:20 +01:00
Append-RunLog " napping: no new room messages or open cases; skipping awake rumination. "
Write-JsonFile -Path $operationOutputPath -Data @ ( )
$finishedAtUtc = [ DateTime ] :: UtcNow
Write-JsonFile -Path $statusPath -Data @ {
status = " skipped "
reason = " napping_without_room_debt "
startedAt = $startedAtUtc . ToString ( " o " )
finishedAt = $finishedAtUtc . ToString ( " o " )
durationSeconds = [ Math ] :: Round ( ( $finishedAtUtc - $startedAtUtc ) . TotalSeconds , 2 )
noPost = [ bool ] $NoPost
skipModel = [ bool ] $SkipModel
stateFile = [ string ] $stateFilePath
contextPath = [ string ] $contextPath
operationOutputPath = [ string ] $operationOutputPath
observedMessageCount = [ int ] $messageCount
openCaseCount = [ int ] $openCaseCount
2026-05-17 19:48:38 +01:00
deliverableCandidateCount = [ int ] $deliverableCandidates . Count
2026-05-17 14:33:20 +01:00
tracePath = [ string ] $tracePath
lastMessagePath = [ string ] $lastMessagePath
}
Remove-Item -Path $lockPath -Force -ErrorAction SilentlyContinue
return
2026-06-09 05:50:01 +01:00
}
2026-05-17 14:33:20 +01:00
}
2026-05-16 19:13:07 +01:00
$stateWriteBefore = if ( Test-Path $stateFilePath ) { ( Get-Item $stateFilePath ) . LastWriteTimeUtc } else { [ DateTime ] :: MinValue }
$prompt = Read-TextFile -Path $promptTemplatePath
$prompt = $prompt . Replace ( " {{CONTEXT_PATH}} " , $contextPath )
$prompt = $prompt . Replace ( " {{STATE_FILE_PATH}} " , $stateFilePath )
$prompt = $prompt . Replace ( " {{OPERATION_OUTPUT_PATH}} " , $operationOutputPath )
$prompt = $prompt . Replace ( " {{NO_POST}} " , ( [ bool ] $NoPost ) . ToString ( ) . ToLowerInvariant ( ) )
$exitCode = 0
$failureMessage = $null
$combinedText = " "
2026-05-05 00:42:25 +01:00
Write-JsonFile -Path $statusPath -Data @ {
2026-05-16 19:13:07 +01:00
status = if ( $SkipModel ) { " running_skip_model " } else { " running " }
startedAt = $startedAtUtc . ToString ( " o " )
noPost = [ bool ] $NoPost
skipModel = [ bool ] $SkipModel
stateFile = $stateFilePath
contextPath = $contextPath
operationOutputPath = $operationOutputPath
codexExecutable = $codexExecutable
codexModel = $codexModel
tracePath = $tracePath
}
if ( $SkipModel ) {
Append-RunLog " skip-model: writing empty operation proposal. "
Write-JsonFile -Path $operationOutputPath -Data @ ( )
$combinedText = " Skipped model rumination by explicit -SkipModel smoke mode. "
} else {
$codexArgs = @ ( )
$codexArgs + = $codexExecArgs
$codexArgs + = @ (
" exec " ,
" --ignore-user-config " ,
" -m " , $codexModel ,
" -c " , 'approval_policy="never"' ,
" -c " , ( " model_reasoning_effort={0} " -f ( ConvertTo-Json $codexReasoningEffort -Compress ) ) ,
" --dangerously-bypass-approvals-and-sandbox " ,
" --skip-git-repo-check " ,
" --json " ,
" -o " , $lastMessagePath ,
" - "
)
2026-05-20 20:31:42 +01:00
$execution = Invoke-CodexExec -Executable $codexExecutable -Arguments $codexArgs -WorkingDirectory $repoRoot -InputText $prompt -TimeoutSeconds $codexTimeoutSeconds
2026-05-16 19:13:07 +01:00
$exitCode = $execution . ExitCode
$combinedText = ( ( $execution . StdOut , $execution . StdErr ) | Where-Object { -not [ string ] :: IsNullOrWhiteSpace ( $_ ) } ) -join [ Environment ] :: NewLine
}
2026-05-19 12:14:53 +01:00
if ( [ string ] :: IsNullOrWhiteSpace ( $combinedText ) ) {
[ System.IO.File ] :: WriteAllText ( $tracePath , " Codex rumination produced no stdout/stderr trace. " + [ Environment ] :: NewLine , [ System.Text.UTF8Encoding ] :: new ( $false ) )
} else {
[ System.IO.File ] :: WriteAllText ( $tracePath , $combinedText + [ Environment ] :: NewLine , [ System.Text.UTF8Encoding ] :: new ( $false ) )
}
2026-05-16 19:13:07 +01:00
if ( $exitCode -ne 0 ) {
$failureMessage = if ( [ string ] :: IsNullOrWhiteSpace ( $combinedText ) ) { " Codex rumination failed with exit code $exitCode . " } else { $combinedText }
throw $failureMessage
}
if ( -not ( Test-Path $operationOutputPath ) ) {
throw " Rumination completed without writing operation output at $operationOutputPath . "
}
Append-RunLog " reading proposed operation output. "
$proposedOperations = @ ( Convert-ToOperationArray -Value ( Read-JsonFile -Path $operationOutputPath ) )
$appliedOperations = @ ( )
2026-06-08 22:54:26 +01:00
$moderationEnforcementResult = $null
2026-05-16 19:13:07 +01:00
2026-06-08 22:26:32 +01:00
Assert-UrgentModerationWitnessesHandled -Witnesses $urgentModerationWitnesses -Operations $proposedOperations -TypedState $typedState
2026-06-08 23:34:35 +01:00
if ( $ModerationHeartbeatOnly ) {
Assert-ModerationHeartbeatOperations -Operations $proposedOperations -TypedState $typedState
} else {
Assert-SpeechPressureObligationsResolved -Obligations $speechPressureObligations -Operations $proposedOperations
}
2026-05-17 19:22:52 +01:00
2026-05-16 19:13:07 +01:00
Append-RunLog ( " applying proposed operations: {0} " -f @ ( $proposedOperations ) . Count )
foreach ( $operation in $proposedOperations ) {
$operationName = Get-ObjectPropertyString -Value $operation -Name " operation "
if ( $null -eq $operation -or $null -eq $operationName ) {
continue
}
2026-05-17 12:25:08 +01:00
Assert-AllowedRuminationOperation -Operation $operation
2026-05-16 19:13:07 +01:00
$appliedOperations + = Apply-TypedOperation -Operation $operation
}
2026-06-08 23:34:35 +01:00
if ( $ModerationHeartbeatOnly ) {
$moderationEnforcementResult = Invoke-ModerationPolicyEnforcement `
-Mode $moderationEnforcementMode `
-DryRun: ( [ bool ] $moderationPolicyDryRun )
} elseif ( -not $NoPost ) {
2026-06-08 22:54:26 +01:00
$moderationEnforcementResult = Invoke-UrgentModerationEnforcement `
-Witnesses $urgentModerationWitnesses `
-Mode $moderationEnforcementMode `
-TimeoutMinutes ( [ int ] $moderationTimeoutMinutes )
}
2026-05-16 19:13:07 +01:00
$observedCursorMessageId = Get-ObjectPropertyString -Value $observedCursor -Name " lastReviewedMessageId "
$observedCursorTimestamp = Get-ObjectPropertyString -Value $observedCursor -Name " lastReviewedTimestamp "
if (
$messageCount -gt 0 -and
$null -ne $observedCursorMessageId -and
$null -ne $observedCursorTimestamp
) {
Append-RunLog " recording reviewed message cursor. "
$appliedOperations + = Apply-TypedOperation -Operation @ {
operation = " record_reviewed_messages "
lastReviewedMessageId = $observedCursorMessageId
lastReviewedTimestamp = $observedCursorTimestamp
}
}
2026-05-18 04:27:17 +01:00
$repoCursorOperations = @ ( )
$disableRepoCursorAdvance = -not [ string ] :: IsNullOrWhiteSpace ( $env:VOID_RUMINATION_DISABLE_REPO_CURSOR_ADVANCE )
2026-06-08 23:34:35 +01:00
if ( -not $ModerationHeartbeatOnly -and -not $NoPost -and -not $SkipModel -and -not $disableRepoCursorAdvance ) {
2026-07-08 21:44:02 +01:00
$handledRepoIds = @ ( Get-HandledRepoIdsFromOperations -Operations $proposedOperations )
if ( $handledRepoIds . Count -gt 0 ) {
Append-RunLog ( " repo activity handled by operations: {0} " -f ( $handledRepoIds -join " , " ) )
} else {
Append-RunLog " repo activity cursor advance skipped: no proposed operation handled a repo target. "
}
$repoCursorOperations = @ ( Get-RepoActivityCursorOperations -RepoActivity $repoActivity -ObservedAt ( [ DateTime ] :: UtcNow ) -HandledRepoIds $handledRepoIds )
2026-05-18 04:27:17 +01:00
if ( $repoCursorOperations . Count -gt 0 ) {
Append-RunLog ( " recording repo activity cursors: {0} " -f $repoCursorOperations . Count )
foreach ( $operation in $repoCursorOperations ) {
$appliedOperations + = Apply-TypedOperation -Operation $operation
}
}
}
2026-05-17 13:56:57 +01:00
$deliveredCandidateCount = 0
2026-06-08 23:34:35 +01:00
if ( -not $ModerationHeartbeatOnly -and -not $NoPost ) {
2026-05-17 19:48:38 +01:00
$refreshedTypedContext = Get-TypedSelfState
2026-05-18 09:47:17 +01:00
$alreadyAnsweredCandidateOperations = @ ( Get-AlreadyAnsweredCandidateRetireOperations -TypedState $refreshedTypedContext . typedState -RetiredAt ( [ DateTime ] :: UtcNow ) )
if ( $alreadyAnsweredCandidateOperations . Count -gt 0 ) {
Append-RunLog ( " retiring already-answered queued candidates: {0} " -f $alreadyAnsweredCandidateOperations . Count )
foreach ( $operation in $alreadyAnsweredCandidateOperations ) {
$appliedOperations + = Apply-TypedOperation -Operation $operation
}
$refreshedTypedContext = Get-TypedSelfState
}
2026-05-17 19:48:38 +01:00
$deliverableCandidates = @ ( Get-DeliverableCandidateInterventions -TypedState $refreshedTypedContext . typedState )
foreach ( $candidate in $deliverableCandidates ) {
2026-05-17 13:56:57 +01:00
if ( $deliveredCandidateCount -ge 1 ) {
break
}
2026-05-17 19:48:38 +01:00
$spokenOperation = Invoke-CandidateInterventionDeliveryFromIntervention -Intervention $candidate
2026-05-17 13:56:57 +01:00
if ( $null -ne $spokenOperation ) {
$appliedOperations + = Apply-TypedOperation -Operation $spokenOperation
2026-05-20 20:31:42 +01:00
Assert-SpokenCandidateApplied -InterventionId $spokenOperation . interventionId -ReceiptKey $spokenOperation . receipt . receiptKey
2026-05-17 13:56:57 +01:00
$deliveredCandidateCount + = 1
}
2026-05-16 19:13:07 +01:00
}
}
Append-RunLog " writing final rumination status. "
$finishedAtUtc = [ DateTime ] :: UtcNow
Append-RunLog " final status timestamp captured. "
2026-05-17 20:17:28 +01:00
$lastMessageSummary = @ (
" Void moderation rumination finished. "
2026-06-08 23:34:35 +01:00
" mode= " + $ ( if ( $ModerationHeartbeatOnly ) { " moderation_heartbeat " } elseif ( $SkipModel ) { " typed_rumination_skip_model " } else { " typed_rumination " } )
2026-05-17 20:17:28 +01:00
" proposed= " + [ string ] @ ( $proposedOperations ) . Count
" applied= " + [ string ] @ ( $appliedOperations ) . Count
" deliveredCandidates= " + [ string ] $deliveredCandidateCount
" finishedAt= " + $finishedAtUtc . ToString ( " o " )
) -join [ Environment ] :: NewLine
[ System.IO.File ] :: WriteAllText ( $lastMessagePath , $lastMessageSummary + [ Environment ] :: NewLine , [ System.Text.UTF8Encoding ] :: new ( $false ) )
$lastMessage = Get-Content -Path $lastMessagePath -Raw -Encoding UTF8
2026-05-16 19:13:07 +01:00
Append-RunLog " final status last message loaded. "
$finalStatus = [ ordered ] @ {
2026-05-16 18:43:53 +01:00
status = " ok "
2026-06-08 23:34:35 +01:00
mode = if ( $ModerationHeartbeatOnly ) { " moderation_heartbeat " } elseif ( $SkipModel ) { " typed_rumination_skip_model " } else { " typed_rumination " }
2026-05-05 00:42:25 +01:00
startedAt = $startedAtUtc . ToString ( " o " )
finishedAt = $finishedAtUtc . ToString ( " o " )
durationSeconds = [ Math ] :: Round ( ( $finishedAtUtc - $startedAtUtc ) . TotalSeconds , 2 )
2026-05-16 19:13:07 +01:00
exitCode = [ int ] $exitCode
2026-05-05 00:42:25 +01:00
noPost = [ bool ] $NoPost
2026-05-16 19:13:07 +01:00
skipModel = [ bool ] $SkipModel
stateFile = [ string ] $stateFilePath
contextPath = [ string ] $contextPath
operationOutputPath = [ string ] $operationOutputPath
observedMessageCount = [ int ] $messageCount
2026-06-09 05:50:01 +01:00
recentRoomLinkCount = [ int ] $recentRoomLinkCount
2026-05-16 19:13:07 +01:00
previousCursorTimestamp = [ string ] ( Get-ObjectPropertyString -Value $priorCursor -Name " lastReviewedTimestamp " )
observedCursorTimestamp = [ string ] $observedCursorTimestamp
proposedOperationCount = [ int ] @ ( $proposedOperations ) . Count
appliedOperationCount = [ int ] @ ( $appliedOperations ) . Count
2026-05-18 04:27:17 +01:00
repoCursorOperationCount = [ int ] @ ( $repoCursorOperations ) . Count
2026-06-08 22:54:26 +01:00
moderationEnforcement = $moderationEnforcementResult
2026-05-17 13:56:57 +01:00
deliveredCandidateCount = [ int ] $deliveredCandidateCount
2026-05-16 19:13:07 +01:00
stateUpdated = [ bool ] ( @ ( $appliedOperations ) . Count -gt 0 )
tracePath = [ string ] $tracePath
lastMessagePath = [ string ] $lastMessagePath
2026-05-17 20:17:28 +01:00
lastMessage = [ string ] $lastMessage
2026-05-05 00:42:25 +01:00
}
2026-05-16 19:13:07 +01:00
Write-JsonFile -Path $statusPath -Data $finalStatus
2026-05-05 00:42:25 +01:00
2026-06-08 23:34:35 +01:00
Append-RunLog ( " mode={0} messages={1} proposed={2} applied={3} cursor={4} " -f $ ( if ( $ModerationHeartbeatOnly ) { " moderation_heartbeat " } else { " typed_rumination " } ) , $messageCount , @ ( $proposedOperations ) . Count , @ ( $appliedOperations ) . Count , $observedCursorTimestamp )
2026-05-05 00:42:25 +01:00
Remove-Item -Path $lockPath -Force -ErrorAction SilentlyContinue