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 ,
[ switch ] $SkipModel
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-05-05 00:42:25 +01:00
$statusPath = Join-Path $statusDir " moderation-rumination.json "
$summaryLogPath = Join-Path $logDir " moderation-rumination.log "
2026-05-16 19:13:07 +01:00
$tracePath = Join-Path $logDir " moderation-rumination-last.jsonl "
$lastMessagePath = Join-Path $statusDir " moderation-rumination-last-message.txt "
$operationOutputPath = Join-Path $statusDir " moderation-rumination-operations.json "
$contextPath = Join-Path $statusDir " moderation-rumination-context.json "
2026-05-05 00:42:25 +01:00
$lockPath = Join-Path $statusDir " moderation-rumination.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-05-16 19:13:07 +01:00
$promptTemplatePath = 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-04 20:28:55 +01:00
$bifrostRoot = if ( -not [ string ] :: IsNullOrWhiteSpace ( $env:BIFROST_ROOT ) ) {
$ExecutionContext . SessionState . Path . GetUnresolvedProviderPathFromPSPath ( $env:BIFROST_ROOT )
} else {
" E:\Projects\Bifrost "
}
$bifrostBridgeScriptPath = if ( -not [ string ] :: IsNullOrWhiteSpace ( $env:VOID_BIFROST_BRIDGE_SCRIPT ) ) {
$ExecutionContext . SessionState . Path . GetUnresolvedProviderPathFromPSPath ( $env:VOID_BIFROST_BRIDGE_SCRIPT )
} else {
Join-Path $bifrostRoot " tools\bifrost-bridge.mjs "
}
2026-06-04 23:37:12 +01:00
$discordTransportMode = " bifrost "
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-06-04 23:29:21 +01:00
function Convert-ToEnvKeySuffix {
param ( [ string ] $Value )
if ( [ string ] :: IsNullOrWhiteSpace ( $Value ) ) {
return " "
}
return ( ( $Value . Trim ( ) . ToUpperInvariant ( ) -replace '[^A-Z0-9]+' , '_' ) -replace '^_+|_+$' , '' )
}
function Resolve-DeliveryPersonaAvatarUrl {
param (
$DeliveryTarget ,
[ string ] $PersonaName
)
$explicitAvatarUrl = Get-ObjectPropertyString -Value $DeliveryTarget -Name " personaAvatarUrl "
if ( -not [ string ] :: IsNullOrWhiteSpace ( $explicitAvatarUrl ) ) {
return $explicitAvatarUrl . Trim ( )
}
if ( -not [ string ] :: IsNullOrWhiteSpace ( $PersonaName ) ) {
$personaSuffix = Convert-ToEnvKeySuffix -Value $PersonaName
if ( -not [ string ] :: IsNullOrWhiteSpace ( $personaSuffix ) ) {
$personaKey = " DISCORD_PERSONA_AVATAR_URL_ $personaSuffix "
$personaAvatarUrl = Get-ConfigValue -Name $personaKey -Values $script:envValues
if ( -not [ string ] :: IsNullOrWhiteSpace ( $personaAvatarUrl ) ) {
return $personaAvatarUrl . Trim ( )
}
}
}
$defaultAvatarUrl = Get-ConfigValue -Name " DISCORD_PERSONA_AVATAR_URL " -Values $script:envValues
if ( -not [ string ] :: IsNullOrWhiteSpace ( $defaultAvatarUrl ) ) {
return $defaultAvatarUrl . Trim ( )
}
if (
-not [ string ] :: IsNullOrWhiteSpace ( $PersonaName ) -and
-not [ string ] :: IsNullOrWhiteSpace ( $script:publicRoomPersonaName ) -and
$PersonaName . Trim ( ) . Equals ( $script:publicRoomPersonaName . Trim ( ) , [ System.StringComparison ] :: OrdinalIgnoreCase ) -and
-not [ string ] :: IsNullOrWhiteSpace ( $script:publicRoomPersonaAvatarUrl )
) {
return $script:publicRoomPersonaAvatarUrl . Trim ( )
}
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 ,
[ DateTime ] $ObservedAt
)
if ( $null -eq $RepoActivity ) {
return @ ( )
}
$repos = @ ( Convert-ToValueArray -Value ( Get-ObjectPropertyValue -Value $RepoActivity -Name " repos " ) )
return @ (
$repos |
Where-Object {
$repoStatus = Get-ObjectPropertyString -Value $_ -Name " status "
$recentCount = Get-ObjectPropertyValue -Value $_ -Name " recentCommitCount "
$latestCommit = Get-ObjectPropertyValue -Value $_ -Name " latestCommit "
$repoStatus -eq " ok " -and
$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-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-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-06-04 21:43:41 +01:00
function Convert-ModerationUserStatusesPatchToOperations {
param ( $Operation )
$userStatuses = Get-ObjectPropertyValue -Value $Operation -Name " userStatuses "
if ( $null -eq $userStatuses ) {
$userStatuses = Get-ObjectPropertyValue -Value $Operation -Name " moderationUserStatuses "
}
if ( $null -eq $userStatuses ) {
return @ ( )
}
return @ (
@ ( Convert-ToValueArray -Value $userStatuses ) |
Where-Object { $null -ne $_ } |
ForEach-Object {
@ {
operation = " upsert_moderation_user_status "
status = $_
}
}
)
}
function Normalize-ProposedRuminationOperations {
param ( $Operations )
$normalized = @ ( )
foreach ( $operation in @ ( Convert-ToOperationArray -Value $Operations ) ) {
if ( $null -eq $operation ) {
continue
}
$operationName = Get-ObjectPropertyString -Value $operation -Name " operation "
if ( $null -ne $operationName ) {
$normalized + = $operation
continue
}
$statusOperations = @ ( Convert-ModerationUserStatusesPatchToOperations -Operation $operation )
if ( $statusOperations . Count -gt 0 ) {
Append-RunLog ( " normalized legacy moderation user status patch into {0} operation(s). " -f $statusOperations . Count )
$normalized + = $statusOperations
continue
}
$normalized + = $operation
}
return @ ( $normalized )
}
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 " ,
2026-06-04 20:28:55 +01:00
" retire_candidate_intervention " ,
" upsert_moderation_user_status "
2026-05-17 12:25:08 +01:00
)
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
}
2026-06-04 23:37:12 +01:00
Assert-BifrostBridgeAvailable
2026-05-17 13:56:57 +01:00
$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 )
$mode = Get-ObjectPropertyString -Value $deliveryTarget -Name " mode "
2026-06-04 23:37:12 +01:00
if ( $mode -eq " owner_dm " ) {
throw " Candidate intervention ' $interventionId ' targets owner_dm, but VoidBot moderation delivery now requires Bifrost Discord transport. "
2026-05-17 13:56:57 +01:00
}
2026-06-04 23:37:12 +01:00
$channelId = Get-ObjectPropertyString -Value $deliveryTarget -Name " channelId "
if ( [ string ] :: IsNullOrWhiteSpace ( $channelId ) ) {
throw " Candidate intervention ' $interventionId ' has deliveryTarget without channelId. "
}
$arguments = @ ( $bifrostBridgeScriptPath , " discord-post " , " --channel-id " , $channelId , " --content-file " , $contentPath )
2026-05-17 13:56:57 +01:00
$replyToMessageId = Get-ObjectPropertyString -Value $deliveryTarget -Name " replyToMessageId "
if ( -not [ string ] :: IsNullOrWhiteSpace ( $replyToMessageId ) ) {
2026-06-04 23:37:12 +01:00
$arguments + = @ ( " --reply-to-message-id " , $replyToMessageId )
2026-05-17 13:56:57 +01:00
}
$personaName = Get-ObjectPropertyString -Value $deliveryTarget -Name " personaName "
if ( -not [ string ] :: IsNullOrWhiteSpace ( $personaName ) ) {
$arguments + = @ ( " --persona-name " , $personaName )
}
2026-06-04 23:29:21 +01:00
$personaAvatarUrl = Resolve-DeliveryPersonaAvatarUrl -DeliveryTarget $deliveryTarget -PersonaName $personaName
2026-05-17 13:56:57 +01:00
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
}
2026-06-04 23:37:12 +01:00
$bridgeReceiptText = [ string ] ( $sendOutput -join [ Environment ] :: NewLine )
$speech = Convert-BifrostBridgeReceiptToSpeechStatus -Receipt ( $bridgeReceiptText | ConvertFrom-Json ) -Preview $draft
2026-05-17 13:56:57 +01:00
return Convert-LastSpeechToSpokenCandidateOperation -InterventionId $interventionId -Speech $speech
}
2026-06-04 20:28:55 +01:00
function Convert-BifrostBridgeReceiptToSpeechStatus {
param (
$Receipt ,
[ string ] $Preview
)
if ( $null -eq $Receipt ) {
return $null
}
2026-06-04 23:37:12 +01:00
$action = Get-ObjectPropertyString -Value $Receipt -Name " action "
$mode = switch ( $action ) {
" discord-post " { " channel " }
" discord-dm " { " user_dm " }
default { $action }
}
2026-06-04 20:28:55 +01:00
return [ pscustomobject ] @ {
sentAt = [ DateTime ] :: UtcNow . ToString ( " o " )
2026-06-04 23:37:12 +01:00
mode = $mode
2026-06-04 20:28:55 +01:00
transport = Get-ObjectPropertyString -Value $Receipt -Name " transport "
channelId = Get-ObjectPropertyString -Value $Receipt -Name " channelId "
messageId = Get-ObjectPropertyString -Value $Receipt -Name " messageId "
recipientId = Get-ObjectPropertyString -Value $Receipt -Name " recipientId "
2026-06-04 23:37:12 +01:00
replyToMessageId = Get-ObjectPropertyString -Value $Receipt -Name " replyToMessageId "
personaName = Get-ObjectPropertyString -Value $Receipt -Name " personaName "
personaAvatarUrl = Get-ObjectPropertyString -Value $Receipt -Name " personaAvatarUrl "
contentLength = if ( $null -ne $Preview ) { [ int ] $Preview . Length } else { $null }
chunkCount = 1
2026-06-04 20:28:55 +01:00
preview = if ( [ string ] :: IsNullOrWhiteSpace ( $Preview ) ) { $null } else { $Preview . Substring ( 0 , [ Math ] :: Min ( 280 , $Preview . Length ) ) }
}
}
2026-06-04 23:37:12 +01:00
function Assert-BifrostBridgeAvailable {
if ( -not ( Test-Path $bifrostBridgeScriptPath ) ) {
throw " Bifrost bridge helper is required for VoidBot moderation delivery but is missing at $bifrostBridgeScriptPath "
}
}
2026-06-04 20:28:55 +01:00
function Get-PendingModerationStatusNotices {
param ( $TypedState )
$statuses = @ ( Convert-ToValueArray -Value $TypedState . moderationCursor . userStatuses )
return @ (
$statuses | ForEach-Object {
$status = $_
$userId = Get-ObjectPropertyString -Value $status -Name " userId "
if ( [ string ] :: IsNullOrWhiteSpace ( $userId ) ) {
return
}
@ ( Convert-ToValueArray -Value ( Get-ObjectPropertyValue -Value $status -Name " pendingNotices " ) ) |
Where-Object { [ string ] :: IsNullOrWhiteSpace ( ( Get-ObjectPropertyString -Value $_ -Name " sentAt " ) ) } |
ForEach-Object {
[ pscustomobject ] @ {
userId = $userId
userName = Get-ObjectPropertyString -Value $status -Name " userName "
notice = $_
}
}
}
)
}
function Convert-LastSpeechToModerationStatusNoticeSentOperation {
param (
[ Parameter ( Mandatory = $true ) ]
[ string ] $UserId ,
[ Parameter ( Mandatory = $true ) ]
[ string ] $NoticeId ,
$Speech
)
$sentAt = Get-ObjectPropertyString -Value $Speech -Name " sentAt "
if ( $null -eq $Speech -or [ string ] :: IsNullOrWhiteSpace ( $sentAt ) ) {
return $null
}
$operation = @ {
operation = " mark_moderation_status_notice_sent "
userId = $UserId
noticeId = $NoticeId
sentAt = $sentAt
}
foreach ( $key in @ ( " channelId " , " messageId " ) ) {
$propertyValue = Get-ObjectPropertyString -Value $Speech -Name $key
if ( $null -ne $propertyValue ) {
$operation [ $key ] = $propertyValue
}
}
return $operation
}
function Invoke-ModerationStatusNoticeDelivery {
param (
[ Parameter ( Mandatory = $true ) ]
$PendingNotice
)
$userId = Get-ObjectPropertyString -Value $PendingNotice -Name " userId "
$notice = Get-ObjectPropertyValue -Value $PendingNotice -Name " notice "
$noticeId = Get-ObjectPropertyString -Value $notice -Name " noticeId "
$body = Get-ObjectPropertyString -Value $notice -Name " body "
if (
[ string ] :: IsNullOrWhiteSpace ( $userId ) -or
[ string ] :: IsNullOrWhiteSpace ( $noticeId ) -or
[ string ] :: IsNullOrWhiteSpace ( $body )
) {
return $null
}
2026-06-04 23:37:12 +01:00
Assert-BifrostBridgeAvailable
2026-06-04 20:28:55 +01:00
$contentPath = Join-Path $statusDir ( " moderation-status-dm-{0}.txt " -f ( [ Guid ] :: NewGuid ( ) . ToString ( " n " ) ) )
$utf8NoBom = [ System.Text.UTF8Encoding ] :: new ( $false )
[ System.IO.File ] :: WriteAllText ( $contentPath , $body , $utf8NoBom )
2026-06-04 23:37:12 +01:00
$arguments = @ ( $bifrostBridgeScriptPath , " discord-dm " , " --recipient-id " , $userId , " --content-file " , $contentPath )
2026-06-04 20:28:55 +01:00
$previousVoidStatusDir = $env:VOID_STATUS_DIR
try {
$env:VOID_STATUS_DIR = $statusDir
$sendOutput = & node @arguments 2 > & 1
if ( $LASTEXITCODE -ne 0 ) {
$sendDetail = [ string ] ( $sendOutput -join [ Environment ] :: NewLine )
if ( [ string ] :: IsNullOrWhiteSpace ( $sendDetail ) ) {
throw " Moderation status DM ' $noticeId ' failed with exit code $LASTEXITCODE . "
}
throw " Moderation status DM ' $noticeId ' failed with exit code $LASTEXITCODE ` : $( $sendDetail . Trim ( ) ) "
}
} finally {
$env:VOID_STATUS_DIR = $previousVoidStatusDir
Remove-Item -LiteralPath $contentPath -Force -ErrorAction SilentlyContinue
}
2026-06-04 23:37:12 +01:00
$bridgeReceiptText = [ string ] ( $sendOutput -join [ Environment ] :: NewLine )
$speech = Convert-BifrostBridgeReceiptToSpeechStatus -Receipt ( $bridgeReceiptText | ConvertFrom-Json ) -Preview $body
2026-06-04 20:28:55 +01:00
return Convert-LastSpeechToModerationStatusNoticeSentOperation -UserId $userId -NoticeId $noticeId -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-04 20:28:55 +01:00
foreach ( $requiredPath in @ ( $contextProjectionScriptPath , $recentHistoryScriptPath , $repoActivityScriptPath , $selfStateScriptPath ) ) {
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-06-04 23:37:12 +01:00
if ( -not [ string ] :: IsNullOrWhiteSpace ( $env:VOID_DISCORD_TRANSPORT ) -and $env:VOID_DISCORD_TRANSPORT . Trim ( ) . ToLowerInvariant ( ) -ne " bifrost " ) {
throw " VOID_DISCORD_TRANSPORT= $( $env:VOID_DISCORD_TRANSPORT ) is no longer supported. VoidBot moderation delivery requires Bifrost. "
2026-06-04 20:28:55 +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-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-06-04 20:28:55 +01:00
$pendingModerationStatusNotices = @ ( Get-PendingModerationStatusNotices -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-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-05-16 19:13:07 +01:00
$repoActivity = $null
try {
$repoActivity = Invoke-NodeJson -Arguments @ (
$repoActivityScriptPath ,
" --hours " , " 96 " ,
" --max-commits " , " 3 " ,
" --read-only " ,
" --state-path " , $stateFilePath
)
} catch {
$repoActivity = @ {
status = " failed "
error = $_ . Exception . Message
}
}
2026-05-20 20:31:42 +01:00
$pendingMentions = @ ( )
$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-16 19:13:07 +01:00
Write-JsonFile -Path $contextPath -Data @ {
2026-05-17 08:52:36 +01:00
generated = " now "
2026-05-16 19:13:07 +01:00
stateFile = $stateFilePath
noPost = [ bool ] $NoPost
selfStateSummary = $typedContext . summary
2026-05-17 08:52:36 +01:00
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 )
2026-06-04 20:28:55 +01:00
moderationUserStatuses = @ ( Project-ModerationUserStatusesForRumination -Statuses $typedState . moderationCursor . userStatuses -Now $startedAtUtc )
2026-05-17 08:52:36 +01:00
speechReceipts = @ ( Project-SpeechReceiptsForRumination -Receipts $typedState . speechReceipts . recentReceipts -Now $startedAtUtc )
memories = @ ( Project-MemoriesForRumination -Memories $typedState . thoughtMemory . memories -Now $startedAtUtc )
2026-05-17 12:25:08 +01:00
shortTermMemories = @ ( Project-MemoriesForRumination -Memories $typedState . thoughtMemory . shortTerm -Now $startedAtUtc )
2026-05-17 08:52:36 +01:00
incubation = @ ( Project-IncubationForRumination -Threads $typedState . thoughtMemory . incubation -Now $startedAtUtc )
2026-05-17 13:02:13 +01:00
agencyPressure = @ ( Project-AgencyPressureForRumination -Pressures $typedState . agencyPressure . pressures -Now $startedAtUtc )
2026-05-17 19:22:52 +01:00
speechPressureObligations = $speechPressureObligations
2026-05-20 20:31:42 +01:00
pendingMentions = $pendingMentions
2026-05-23 11:12:18 +01:00
recentConversationTarget = Project-RecentConversationTargetForRumination -History $history -PersonaName $publicRoomPersonaName -PersonaAvatarUrl $publicRoomPersonaAvatarUrl
2026-05-18 04:27:17 +01:00
publicSpeechTarget = Project-PublicSpeechTargetForRumination -ChannelId $publicRoomChannelId -PersonaName $publicRoomPersonaName -PersonaAvatarUrl $publicRoomPersonaAvatarUrl
2026-05-17 19:48:38 +01:00
deliverableCandidateCount = [ int ] $deliverableCandidates . Count
2026-05-17 08:52:36 +01:00
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-04 20:28:55 +01:00
if ( $isNapping -and $messageCount -eq 0 -and $openCaseCount -eq 0 -and $deliverableCandidates . Count -eq 0 -and $pendingModerationStatusNotices . Count -eq 0 ) {
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-06-04 20:28:55 +01:00
pendingModerationStatusNoticeCount = [ int ] $pendingModerationStatusNotices . Count
2026-05-17 14:33:20 +01:00
tracePath = [ string ] $tracePath
lastMessagePath = [ string ] $lastMessagePath
}
Remove-Item -Path $lockPath -Force -ErrorAction SilentlyContinue
return
}
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. "
2026-06-04 21:43:41 +01:00
$proposedOperations = @ ( Normalize-ProposedRuminationOperations -Operations ( Read-JsonFile -Path $operationOutputPath ) )
2026-05-16 19:13:07 +01:00
$appliedOperations = @ ( )
2026-05-17 19:22:52 +01:00
Assert-SpeechPressureObligationsResolved -Obligations $speechPressureObligations -Operations $proposedOperations
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
}
$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 )
if ( -not $NoPost -and -not $SkipModel -and -not $disableRepoCursorAdvance ) {
$repoCursorOperations = @ ( Get-RepoActivityCursorOperations -RepoActivity $repoActivity -ObservedAt ( [ DateTime ] :: UtcNow ) )
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-04 20:28:55 +01:00
$deliveredModerationStatusNoticeCount = 0
2026-05-17 13:56:57 +01:00
if ( -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
}
2026-06-04 20:28:55 +01:00
$refreshedTypedContext = Get-TypedSelfState
$pendingModerationStatusNotices = @ ( Get-PendingModerationStatusNotices -TypedState $refreshedTypedContext . typedState )
foreach ( $pendingNotice in $pendingModerationStatusNotices ) {
if ( $deliveredModerationStatusNoticeCount -ge 3 ) {
break
}
$noticeOperation = Invoke-ModerationStatusNoticeDelivery -PendingNotice $pendingNotice
if ( $null -ne $noticeOperation ) {
$appliedOperations + = Apply-TypedOperation -Operation $noticeOperation
$deliveredModerationStatusNoticeCount + = 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. "
" mode= " + $ ( if ( $SkipModel ) { " typed_rumination_skip_model " } else { " typed_rumination " } )
" proposed= " + [ string ] @ ( $proposedOperations ) . Count
" applied= " + [ string ] @ ( $appliedOperations ) . Count
" deliveredCandidates= " + [ string ] $deliveredCandidateCount
2026-06-04 20:28:55 +01:00
" deliveredModerationStatusNotices= " + [ string ] $deliveredModerationStatusNoticeCount
" discordTransport= " + [ string ] $discordTransportMode
2026-05-17 20:17:28 +01:00
" 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-05-16 19:13:07 +01:00
mode = if ( $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
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-05-17 13:56:57 +01:00
deliveredCandidateCount = [ int ] $deliveredCandidateCount
2026-06-04 20:28:55 +01:00
deliveredModerationStatusNoticeCount = [ int ] $deliveredModerationStatusNoticeCount
discordTransport = [ string ] $discordTransportMode
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-05-16 19:13:07 +01:00
Append-RunLog ( " mode=typed_rumination messages={0} proposed={1} applied={2} cursor={3} " -f $messageCount , @ ( $proposedOperations ) . Count , @ ( $appliedOperations ) . Count , $observedCursorTimestamp )
2026-05-05 00:42:25 +01:00
Remove-Item -Path $lockPath -Force -ErrorAction SilentlyContinue