Delete local Void mood and memory runners
This commit is contained in:
parent
5b35fd9fd2
commit
04dc8e3303
16 changed files with 47 additions and 2408 deletions
|
|
@ -1,89 +0,0 @@
|
|||
param(
|
||||
[string] $TaskName,
|
||||
[int] $IntervalMinutes,
|
||||
[switch] $RunNow
|
||||
)
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = "Stop"
|
||||
$ProgressPreference = "SilentlyContinue"
|
||||
|
||||
$repoRoot = Split-Path -Parent $PSScriptRoot
|
||||
$envFile = Join-Path $repoRoot ".env"
|
||||
$runnerScript = Join-Path $PSScriptRoot "run-void-mood-drift.ps1"
|
||||
$hiddenLauncher = Join-Path $PSScriptRoot "run-hidden-powershell.vbs"
|
||||
|
||||
function Read-DotEnv {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string] $Path
|
||||
)
|
||||
|
||||
$values = @{}
|
||||
|
||||
if (-not (Test-Path $Path)) {
|
||||
return $values
|
||||
}
|
||||
|
||||
foreach ($line in Get-Content -Path $Path) {
|
||||
if ([string]::IsNullOrWhiteSpace($line)) {
|
||||
continue
|
||||
}
|
||||
|
||||
if ($line.TrimStart().StartsWith("#")) {
|
||||
continue
|
||||
}
|
||||
|
||||
$separatorIndex = $line.IndexOf("=")
|
||||
|
||||
if ($separatorIndex -lt 1) {
|
||||
continue
|
||||
}
|
||||
|
||||
$key = $line.Substring(0, $separatorIndex).Trim()
|
||||
$value = $line.Substring($separatorIndex + 1)
|
||||
$values[$key] = $value
|
||||
}
|
||||
|
||||
return $values
|
||||
}
|
||||
|
||||
$config = Read-DotEnv -Path $envFile
|
||||
$taskNameValue = if (-not [string]::IsNullOrWhiteSpace($TaskName)) {
|
||||
$TaskName
|
||||
} elseif ($config.ContainsKey("VOIDBOT_MOOD_TASK_NAME") -and -not [string]::IsNullOrWhiteSpace($config["VOIDBOT_MOOD_TASK_NAME"])) {
|
||||
$config["VOIDBOT_MOOD_TASK_NAME"]
|
||||
} else {
|
||||
"Void Mood Drift"
|
||||
}
|
||||
$intervalMinutesValue = if ($PSBoundParameters.ContainsKey("IntervalMinutes")) {
|
||||
$IntervalMinutes
|
||||
} elseif ($config.ContainsKey("VOIDBOT_MOOD_INTERVAL_MINUTES") -and -not [string]::IsNullOrWhiteSpace($config["VOIDBOT_MOOD_INTERVAL_MINUTES"])) {
|
||||
[int]$config["VOIDBOT_MOOD_INTERVAL_MINUTES"]
|
||||
} else {
|
||||
5
|
||||
}
|
||||
|
||||
if ($intervalMinutesValue -lt 5) {
|
||||
throw "VOIDBOT_MOOD_INTERVAL_MINUTES must be at least 5."
|
||||
}
|
||||
|
||||
$startAt = (Get-Date).AddMinutes(1)
|
||||
$action = New-ScheduledTaskAction -Execute "wscript.exe" -Argument "//B //nologo `"$hiddenLauncher`" -NoProfile -NonInteractive -WindowStyle Hidden -ExecutionPolicy Bypass -File `"$runnerScript`""
|
||||
$trigger = New-ScheduledTaskTrigger -Once -At $startAt -RepetitionInterval (New-TimeSpan -Minutes $intervalMinutesValue) -RepetitionDuration (New-TimeSpan -Days 3650)
|
||||
$settings = New-ScheduledTaskSettingsSet -StartWhenAvailable -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -ExecutionTimeLimit (New-TimeSpan -Minutes 3) -MultipleInstances IgnoreNew
|
||||
$principal = New-ScheduledTaskPrincipal -UserId "$env:USERDOMAIN\$env:USERNAME" -LogonType Interactive -RunLevel Limited
|
||||
$description = "Runs Void's background mood-drift pass every $intervalMinutesValue minutes so current activations and speaking pressure do not stay emotionally flat between moderation loops."
|
||||
|
||||
Register-ScheduledTask -TaskName $taskNameValue -Action $action -Trigger $trigger -Settings $settings -Principal $principal -Description $description -Force | Out-Null
|
||||
|
||||
Write-Host "Installed scheduled task: $taskNameValue"
|
||||
Write-Host "Schedule: every $intervalMinutesValue minute(s), starting at $($startAt.ToString('yyyy-MM-dd HH:mm'))"
|
||||
Write-Host "Task runs as: $($env:USERDOMAIN)\$($env:USERNAME)"
|
||||
Write-Host "Execution time limit: 3 minutes"
|
||||
Write-Host "Runner script: $runnerScript"
|
||||
|
||||
if ($RunNow) {
|
||||
Start-ScheduledTask -TaskName $taskNameValue
|
||||
Write-Host "Started task immediately."
|
||||
}
|
||||
|
|
@ -1,744 +0,0 @@
|
|||
Set-StrictMode -Version Latest
|
||||
|
||||
function Convert-ToValueArray {
|
||||
param($Value)
|
||||
|
||||
if ($null -eq $Value) {
|
||||
return @()
|
||||
}
|
||||
|
||||
if ($Value -is [System.Array]) {
|
||||
return @($Value)
|
||||
}
|
||||
|
||||
return @($Value)
|
||||
}
|
||||
|
||||
function Get-ObjectPropertyValue {
|
||||
param(
|
||||
$Value,
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string] $Name
|
||||
)
|
||||
|
||||
if ($null -eq $Value) {
|
||||
return $null
|
||||
}
|
||||
|
||||
if ($Value -is [System.Collections.IDictionary]) {
|
||||
if ($Value.Contains($Name)) {
|
||||
return $Value[$Name]
|
||||
}
|
||||
return $null
|
||||
}
|
||||
|
||||
$property = $Value.PSObject.Properties[$Name]
|
||||
if ($null -eq $property) {
|
||||
return $null
|
||||
}
|
||||
return $property.Value
|
||||
}
|
||||
|
||||
function Get-ObjectPropertyString {
|
||||
param(
|
||||
$Value,
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string] $Name
|
||||
)
|
||||
|
||||
$value = Get-ObjectPropertyValue -Value $Value -Name $Name
|
||||
if ($null -eq $value) {
|
||||
return $null
|
||||
}
|
||||
|
||||
$text = [string]$value
|
||||
if ([string]::IsNullOrWhiteSpace($text)) {
|
||||
return $null
|
||||
}
|
||||
return $text
|
||||
}
|
||||
|
||||
function Format-RelativeTime {
|
||||
param(
|
||||
[string] $Value,
|
||||
[Parameter(Mandatory = $true)]
|
||||
[DateTime] $Now
|
||||
)
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($Value)) {
|
||||
return $null
|
||||
}
|
||||
|
||||
$timestamp = [DateTime]::MinValue
|
||||
if (-not [DateTime]::TryParse($Value, [Globalization.CultureInfo]::InvariantCulture, [Globalization.DateTimeStyles]::AssumeUniversal, [ref]$timestamp)) {
|
||||
return $null
|
||||
}
|
||||
|
||||
$timestampUtc = $timestamp.ToUniversalTime()
|
||||
$delta = $timestampUtc - $Now.ToUniversalTime()
|
||||
$future = $delta.TotalSeconds -gt 0
|
||||
$seconds = [Math]::Abs($delta.TotalSeconds)
|
||||
|
||||
if ($seconds -lt 45) {
|
||||
return "just now"
|
||||
}
|
||||
|
||||
if ($seconds -lt 5400) {
|
||||
$minutes = [Math]::Max(1, [int][Math]::Round($seconds / 60))
|
||||
$unit = if ($minutes -eq 1) { "minute" } else { "minutes" }
|
||||
$phrase = "$minutes $unit"
|
||||
} elseif ($seconds -lt 129600) {
|
||||
$hours = [Math]::Max(1, [int][Math]::Round($seconds / 3600))
|
||||
$unit = if ($hours -eq 1) { "hour" } else { "hours" }
|
||||
$phrase = "$hours $unit"
|
||||
} elseif ($seconds -lt 1814400) {
|
||||
$days = [Math]::Max(1, [int][Math]::Round($seconds / 86400))
|
||||
$unit = if ($days -eq 1) { "day" } else { "days" }
|
||||
$phrase = "$days $unit"
|
||||
} elseif ($seconds -lt 6048000) {
|
||||
$weeks = [Math]::Max(1, [int][Math]::Round($seconds / 604800))
|
||||
$unit = if ($weeks -eq 1) { "week" } else { "weeks" }
|
||||
$phrase = "$weeks $unit"
|
||||
} elseif ($seconds -lt 47304000) {
|
||||
$months = [Math]::Max(1, [int][Math]::Round($seconds / 2592000))
|
||||
$unit = if ($months -eq 1) { "month" } else { "months" }
|
||||
$phrase = "$months $unit"
|
||||
} else {
|
||||
$years = [Math]::Max(1, [int][Math]::Round($seconds / 31536000))
|
||||
$unit = if ($years -eq 1) { "year" } else { "years" }
|
||||
$phrase = "$years $unit"
|
||||
}
|
||||
|
||||
if ($future) {
|
||||
return "in $phrase"
|
||||
}
|
||||
return "$phrase ago"
|
||||
}
|
||||
|
||||
function Project-RelativeTimestamp {
|
||||
param(
|
||||
$Value,
|
||||
[string] $Name,
|
||||
[DateTime] $Now
|
||||
)
|
||||
|
||||
return Format-RelativeTime -Value (Get-ObjectPropertyString -Value $Value -Name $Name) -Now $Now
|
||||
}
|
||||
|
||||
function Project-RecentHistoryForRumination {
|
||||
param($History, [DateTime] $Now)
|
||||
|
||||
$messages = @(Convert-ToValueArray -Value (Get-ObjectPropertyValue -Value $History -Name "messages"))
|
||||
return @{
|
||||
source = "discord_history"
|
||||
messageCount = [int]$messages.Count
|
||||
messages = @(
|
||||
$messages | ForEach-Object {
|
||||
@{
|
||||
id = Get-ObjectPropertyString -Value $_ -Name "id"
|
||||
channelId = Get-ObjectPropertyString -Value $_ -Name "channelId"
|
||||
authorId = Get-ObjectPropertyString -Value $_ -Name "authorId"
|
||||
authorName = Get-ObjectPropertyString -Value $_ -Name "authorName"
|
||||
isBot = [bool](Get-ObjectPropertyValue -Value $_ -Name "isBot")
|
||||
when = Project-RelativeTimestamp -Value $_ -Name "timestamp" -Now $Now
|
||||
content = Get-ObjectPropertyString -Value $_ -Name "content"
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
function Project-RecentConversationTargetForRumination {
|
||||
param(
|
||||
$History,
|
||||
[string] $PersonaName,
|
||||
[string] $PersonaAvatarUrl
|
||||
)
|
||||
|
||||
$messages = @(Convert-ToValueArray -Value (Get-ObjectPropertyValue -Value $History -Name "messages"))
|
||||
if ($messages.Count -eq 0) {
|
||||
return $null
|
||||
}
|
||||
|
||||
$anchor = $null
|
||||
for ($index = $messages.Count - 1; $index -ge 0; $index--) {
|
||||
$candidate = $messages[$index]
|
||||
$channelId = Get-ObjectPropertyString -Value $candidate -Name "channelId"
|
||||
if ([string]::IsNullOrWhiteSpace($channelId)) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (-not [bool](Get-ObjectPropertyValue -Value $candidate -Name "isBot")) {
|
||||
$anchor = $candidate
|
||||
break
|
||||
}
|
||||
|
||||
if ($null -eq $anchor) {
|
||||
$anchor = $candidate
|
||||
}
|
||||
}
|
||||
|
||||
if ($null -eq $anchor) {
|
||||
return $null
|
||||
}
|
||||
|
||||
$target = @{
|
||||
mode = "channel"
|
||||
channelId = Get-ObjectPropertyString -Value $anchor -Name "channelId"
|
||||
replyToMessageId = Get-ObjectPropertyString -Value $anchor -Name "id"
|
||||
personaName = if ([string]::IsNullOrWhiteSpace($PersonaName)) { "Void" } else { $PersonaName.Trim() }
|
||||
reason = "Latest concrete recent-history conversation anchor. Use this instead of the configured public room when replying to that conversation."
|
||||
}
|
||||
|
||||
if (-not [string]::IsNullOrWhiteSpace($PersonaAvatarUrl)) {
|
||||
$target.personaAvatarUrl = $PersonaAvatarUrl.Trim()
|
||||
}
|
||||
|
||||
return $target
|
||||
}
|
||||
|
||||
function Get-UrlsFromText {
|
||||
param([string] $Text)
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($Text)) {
|
||||
return @()
|
||||
}
|
||||
|
||||
return @(
|
||||
[regex]::Matches($Text, "https?://[^\s<>()]+") |
|
||||
ForEach-Object { $_.Value.TrimEnd(".", ",", "!", "?", ":", ";", ")", "]") } |
|
||||
Where-Object { -not [string]::IsNullOrWhiteSpace($_) }
|
||||
)
|
||||
}
|
||||
|
||||
function Resolve-GameCultUrlSource {
|
||||
param([string] $Url)
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($Url)) {
|
||||
return $null
|
||||
}
|
||||
|
||||
$uri = $null
|
||||
if (-not [System.Uri]::TryCreate($Url, [System.UriKind]::Absolute, [ref]$uri)) {
|
||||
return $null
|
||||
}
|
||||
|
||||
$uriHost = $uri.Host.ToLowerInvariant()
|
||||
if ($uriHost -notin @("gamecult.org", "www.gamecult.org", "gamecult.games", "www.gamecult.games")) {
|
||||
return $null
|
||||
}
|
||||
|
||||
$segments = @(
|
||||
$uri.AbsolutePath.Trim("/") -split "/" |
|
||||
Where-Object { -not [string]::IsNullOrWhiteSpace($_) } |
|
||||
ForEach-Object { [System.Uri]::UnescapeDataString($_) }
|
||||
)
|
||||
|
||||
if ($segments.Count -lt 2 -or $segments[0].ToLowerInvariant() -ne "blog") {
|
||||
return @{
|
||||
kind = "gamecult_url"
|
||||
host = $uriHost
|
||||
path = $uri.AbsolutePath
|
||||
}
|
||||
}
|
||||
|
||||
$slugParts = @($segments | Select-Object -Skip 1)
|
||||
$relativePath = "GameCult/Blog/$($slugParts -join "/")"
|
||||
$markdownPath = "$relativePath.md"
|
||||
if ($slugParts.Count -gt 1) {
|
||||
$markdownPath = "$relativePath/index.md"
|
||||
}
|
||||
|
||||
return @{
|
||||
kind = "gamecult_blog"
|
||||
host = $uriHost
|
||||
slug = ($slugParts -join "/")
|
||||
repoName = "gamecult-site"
|
||||
path = $markdownPath
|
||||
sourceRef = "gamecult-site:$markdownPath"
|
||||
}
|
||||
}
|
||||
|
||||
function Project-RecentRoomLinksForRumination {
|
||||
param(
|
||||
$History,
|
||||
[DateTime] $Now,
|
||||
[string] $MinTimestampInclusive,
|
||||
[object[]] $ReceiptReplyMessageIds
|
||||
)
|
||||
|
||||
$messages = @(Convert-ToValueArray -Value (Get-ObjectPropertyValue -Value $History -Name "messages"))
|
||||
$answeredMessageIds = [System.Collections.Generic.HashSet[string]]::new()
|
||||
foreach ($replyMessageId in @(Convert-ToValueArray -Value $ReceiptReplyMessageIds)) {
|
||||
if (-not [string]::IsNullOrWhiteSpace([string]$replyMessageId)) {
|
||||
[void]$answeredMessageIds.Add([string]$replyMessageId)
|
||||
}
|
||||
}
|
||||
|
||||
$minTimestampMs = $null
|
||||
if (-not [string]::IsNullOrWhiteSpace($MinTimestampInclusive)) {
|
||||
$parsedMinTimestamp = [DateTime]::MinValue
|
||||
if ([DateTime]::TryParse($MinTimestampInclusive, [Globalization.CultureInfo]::InvariantCulture, [Globalization.DateTimeStyles]::AssumeUniversal, [ref]$parsedMinTimestamp)) {
|
||||
$minTimestampMs = $parsedMinTimestamp.ToUniversalTime()
|
||||
}
|
||||
}
|
||||
|
||||
$items = @()
|
||||
foreach ($message in $messages) {
|
||||
if ([bool](Get-ObjectPropertyValue -Value $message -Name "isBot")) {
|
||||
continue
|
||||
}
|
||||
|
||||
$messageId = Get-ObjectPropertyString -Value $message -Name "id"
|
||||
if (-not [string]::IsNullOrWhiteSpace($messageId) -and $answeredMessageIds.Contains($messageId)) {
|
||||
continue
|
||||
}
|
||||
|
||||
$cursorAlreadyPassed = $false
|
||||
if ($null -ne $minTimestampMs) {
|
||||
$messageTimestamp = [DateTime]::MinValue
|
||||
$rawTimestamp = Get-ObjectPropertyString -Value $message -Name "timestamp"
|
||||
if (
|
||||
[string]::IsNullOrWhiteSpace($rawTimestamp) -or
|
||||
-not [DateTime]::TryParse($rawTimestamp, [Globalization.CultureInfo]::InvariantCulture, [Globalization.DateTimeStyles]::AssumeUniversal, [ref]$messageTimestamp)
|
||||
) {
|
||||
continue
|
||||
}
|
||||
|
||||
$messageTimestampUtc = $messageTimestamp.ToUniversalTime()
|
||||
if ($messageTimestampUtc -lt $minTimestampMs) {
|
||||
if ($minTimestampMs -gt $Now.ToUniversalTime().AddMinutes(1)) {
|
||||
continue
|
||||
}
|
||||
$cursorAlreadyPassed = $true
|
||||
}
|
||||
}
|
||||
|
||||
$content = Get-ObjectPropertyString -Value $message -Name "content"
|
||||
$urls = @(Get-UrlsFromText -Text $content)
|
||||
if ($urls.Count -eq 0) {
|
||||
continue
|
||||
}
|
||||
|
||||
foreach ($url in $urls) {
|
||||
$items += @{
|
||||
kind = "naked_or_embedded_url"
|
||||
id = $messageId
|
||||
channelId = Get-ObjectPropertyString -Value $message -Name "channelId"
|
||||
authorId = Get-ObjectPropertyString -Value $message -Name "authorId"
|
||||
authorName = Get-ObjectPropertyString -Value $message -Name "authorName"
|
||||
when = Project-RelativeTimestamp -Value $message -Name "timestamp" -Now $Now
|
||||
content = $content
|
||||
url = $url
|
||||
resolvedSource = Resolve-GameCultUrlSource -Url $url
|
||||
carryForwardReason = if ($cursorAlreadyPassed) { "safety cursor already reviewed this message, but no speech receipt answers the URL object yet" } else { "at or after current reviewed cursor" }
|
||||
attentionContract = "A human posting a naked URL is still presenting an object to inspect. Treat it as room context, not as empty speech."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return @{
|
||||
source = "recent_discord_url_scan"
|
||||
itemCount = [int]$items.Count
|
||||
lookback = Get-ObjectPropertyValue -Value $History -Name "hours"
|
||||
items = $items
|
||||
}
|
||||
}
|
||||
|
||||
function Project-OpenCasesForRumination {
|
||||
param($Cases, [DateTime] $Now)
|
||||
|
||||
return @(
|
||||
@(Convert-ToValueArray -Value $Cases) |
|
||||
Where-Object {
|
||||
-not (Test-RuminationCaseTerminal -Status (Get-ObjectPropertyString -Value $_ -Name "status"))
|
||||
} |
|
||||
ForEach-Object {
|
||||
@{
|
||||
sourceMessageId = Get-ObjectPropertyString -Value $_ -Name "sourceMessageId"
|
||||
status = Get-ObjectPropertyString -Value $_ -Name "status"
|
||||
summary = Get-ObjectPropertyString -Value $_ -Name "summary"
|
||||
authorName = Get-ObjectPropertyString -Value $_ -Name "authorName"
|
||||
channelId = Get-ObjectPropertyString -Value $_ -Name "channelId"
|
||||
messageUrl = Get-ObjectPropertyString -Value $_ -Name "messageUrl"
|
||||
whyItMatters = Get-ObjectPropertyString -Value $_ -Name "whyItMatters"
|
||||
created = Project-RelativeTimestamp -Value $_ -Name "createdAt" -Now $Now
|
||||
lastTouched = Project-RelativeTimestamp -Value $_ -Name "lastTouchedAt" -Now $Now
|
||||
resolved = Project-RelativeTimestamp -Value $_ -Name "resolvedAt" -Now $Now
|
||||
tags = @(Convert-ToValueArray -Value (Get-ObjectPropertyValue -Value $_ -Name "tags"))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
function Test-RuminationCaseTerminal {
|
||||
param([string] $Status)
|
||||
|
||||
return @("answered", "resolved", "closed", "retired", "dropped").Contains($Status)
|
||||
}
|
||||
|
||||
function Project-SpeechReceiptsForRumination {
|
||||
param($Receipts, [DateTime] $Now)
|
||||
|
||||
return @(
|
||||
@(Convert-ToValueArray -Value $Receipts) | ForEach-Object {
|
||||
@{
|
||||
receiptKey = Get-ObjectPropertyString -Value $_ -Name "receiptKey"
|
||||
candidateInterventionId = Get-ObjectPropertyString -Value $_ -Name "candidateInterventionId"
|
||||
sent = Project-RelativeTimestamp -Value $_ -Name "sentAt" -Now $Now
|
||||
mode = Get-ObjectPropertyString -Value $_ -Name "mode"
|
||||
transport = Get-ObjectPropertyString -Value $_ -Name "transport"
|
||||
channelId = Get-ObjectPropertyString -Value $_ -Name "channelId"
|
||||
replyToMessageId = Get-ObjectPropertyString -Value $_ -Name "replyToMessageId"
|
||||
preview = Get-ObjectPropertyString -Value $_ -Name "preview"
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
function Project-MemoriesForRumination {
|
||||
param($Memories, [DateTime] $Now)
|
||||
|
||||
return @(
|
||||
@(Convert-ToValueArray -Value $Memories) | ForEach-Object {
|
||||
@{
|
||||
memoryId = Get-ObjectPropertyString -Value $_ -Name "memoryId"
|
||||
kind = Get-ObjectPropertyString -Value $_ -Name "kind"
|
||||
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"
|
||||
created = Project-RelativeTimestamp -Value $_ -Name "createdAt" -Now $Now
|
||||
updated = Project-RelativeTimestamp -Value $_ -Name "updatedAt" -Now $Now
|
||||
retired = Project-RelativeTimestamp -Value $_ -Name "retiredAt" -Now $Now
|
||||
anchorRefs = @(Convert-ToValueArray -Value (Get-ObjectPropertyValue -Value $_ -Name "anchorRefs"))
|
||||
evidenceRefs = @(Convert-ToValueArray -Value (Get-ObjectPropertyValue -Value $_ -Name "evidenceRefs"))
|
||||
tags = @(Convert-ToValueArray -Value (Get-ObjectPropertyValue -Value $_ -Name "tags"))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
function Project-IncubationForRumination {
|
||||
param($Threads, [DateTime] $Now)
|
||||
|
||||
return @(
|
||||
@(Convert-ToValueArray -Value $Threads) | ForEach-Object {
|
||||
@{
|
||||
threadId = Get-ObjectPropertyString -Value $_ -Name "threadId"
|
||||
target = Get-ObjectPropertyValue -Value $_ -Name "target"
|
||||
topic = Get-ObjectPropertyString -Value $_ -Name "topic"
|
||||
summary = Get-ObjectPropertyString -Value $_ -Name "summary"
|
||||
supportMemoryIds = @(Convert-ToValueArray -Value (Get-ObjectPropertyValue -Value $_ -Name "supportMemoryIds"))
|
||||
anchorRefs = @(Convert-ToValueArray -Value (Get-ObjectPropertyValue -Value $_ -Name "anchorRefs"))
|
||||
evidenceRefs = @(Convert-ToValueArray -Value (Get-ObjectPropertyValue -Value $_ -Name "evidenceRefs"))
|
||||
maturation = Get-ObjectPropertyValue -Value $_ -Name "maturation"
|
||||
noveltyToRoom = Get-ObjectPropertyValue -Value $_ -Name "noveltyToRoom"
|
||||
noveltyToSelf = Get-ObjectPropertyValue -Value $_ -Name "noveltyToSelf"
|
||||
desireToSpeak = Get-ObjectPropertyValue -Value $_ -Name "desireToSpeak"
|
||||
saturationScore = Get-ObjectPropertyValue -Value $_ -Name "saturationScore"
|
||||
status = Get-ObjectPropertyString -Value $_ -Name "status"
|
||||
created = Project-RelativeTimestamp -Value $_ -Name "createdAt" -Now $Now
|
||||
updated = Project-RelativeTimestamp -Value $_ -Name "updatedAt" -Now $Now
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
function Project-InterventionsForRumination {
|
||||
param($Interventions, [DateTime] $Now)
|
||||
|
||||
return @(
|
||||
@(Convert-ToValueArray -Value $Interventions) | ForEach-Object {
|
||||
@{
|
||||
interventionId = Get-ObjectPropertyString -Value $_ -Name "interventionId"
|
||||
kind = Get-ObjectPropertyString -Value $_ -Name "kind"
|
||||
status = Get-ObjectPropertyString -Value $_ -Name "status"
|
||||
target = Get-ObjectPropertyValue -Value $_ -Name "target"
|
||||
summary = Get-ObjectPropertyString -Value $_ -Name "summary"
|
||||
draft = Get-ObjectPropertyString -Value $_ -Name "draft"
|
||||
priority = Get-ObjectPropertyValue -Value $_ -Name "priority"
|
||||
mustEventuallyShare = Get-ObjectPropertyValue -Value $_ -Name "mustEventuallyShare"
|
||||
created = Project-RelativeTimestamp -Value $_ -Name "createdAt" -Now $Now
|
||||
updated = Project-RelativeTimestamp -Value $_ -Name "updatedAt" -Now $Now
|
||||
spoken = Project-RelativeTimestamp -Value $_ -Name "spokenAt" -Now $Now
|
||||
retired = Project-RelativeTimestamp -Value $_ -Name "retiredAt" -Now $Now
|
||||
tags = @(Convert-ToValueArray -Value (Get-ObjectPropertyValue -Value $_ -Name "tags"))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
function Project-PublicSpeechTargetForRumination {
|
||||
param(
|
||||
[string] $ChannelId,
|
||||
[string] $PersonaName,
|
||||
[string] $PersonaAvatarUrl
|
||||
)
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($ChannelId)) {
|
||||
return $null
|
||||
}
|
||||
|
||||
$target = @{
|
||||
mode = "channel"
|
||||
channelId = $ChannelId.Trim()
|
||||
personaName = if ([string]::IsNullOrWhiteSpace($PersonaName)) { "Void" } else { $PersonaName.Trim() }
|
||||
reason = "Configured public room for Void to present self-authored public artifacts and other ripe public thoughts."
|
||||
}
|
||||
|
||||
if (-not [string]::IsNullOrWhiteSpace($PersonaAvatarUrl)) {
|
||||
$target.personaAvatarUrl = $PersonaAvatarUrl.Trim()
|
||||
}
|
||||
|
||||
return $target
|
||||
}
|
||||
|
||||
function Project-AgencyPressureForRumination {
|
||||
param($Pressures, [DateTime] $Now)
|
||||
|
||||
return @(
|
||||
@(Convert-ToValueArray -Value $Pressures) | 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"
|
||||
created = Project-RelativeTimestamp -Value $_ -Name "createdAt" -Now $Now
|
||||
updated = Project-RelativeTimestamp -Value $_ -Name "updatedAt" -Now $Now
|
||||
resolved = Project-RelativeTimestamp -Value $_ -Name "resolvedAt" -Now $Now
|
||||
retired = Project-RelativeTimestamp -Value $_ -Name "retiredAt" -Now $Now
|
||||
anchorRefs = @(Convert-ToValueArray -Value (Get-ObjectPropertyValue -Value $_ -Name "anchorRefs"))
|
||||
evidenceRefs = @(Convert-ToValueArray -Value (Get-ObjectPropertyValue -Value $_ -Name "evidenceRefs"))
|
||||
sourceMemoryIds = @(Convert-ToValueArray -Value (Get-ObjectPropertyValue -Value $_ -Name "sourceMemoryIds"))
|
||||
tags = @(Convert-ToValueArray -Value (Get-ObjectPropertyValue -Value $_ -Name "tags"))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
function Project-ScheduledRuntimeForRumination {
|
||||
param($Runtime, [DateTime] $Now)
|
||||
|
||||
$sleepCycle = Get-ObjectPropertyValue -Value $Runtime -Name "sleepCycle"
|
||||
$speakingPressure = Get-ObjectPropertyValue -Value $Runtime -Name "speakingPressure"
|
||||
$lastRuns = @(Convert-ToValueArray -Value (Get-ObjectPropertyValue -Value $Runtime -Name "lastRuns"))
|
||||
|
||||
return @{
|
||||
sleepCycle = @{
|
||||
isNapping = [bool](Get-ObjectPropertyValue -Value $sleepCycle -Name "isNapping")
|
||||
currentNapStarted = Project-RelativeTimestamp -Value $sleepCycle -Name "currentNapStartedAt" -Now $Now
|
||||
currentNapEnds = Project-RelativeTimestamp -Value $sleepCycle -Name "currentNapEndsAt" -Now $Now
|
||||
nextNapStarts = Project-RelativeTimestamp -Value $sleepCycle -Name "nextNapStartsAt" -Now $Now
|
||||
activeDreamThemes = @(Convert-ToValueArray -Value (Get-ObjectPropertyValue -Value $sleepCycle -Name "activeDreamThemes"))
|
||||
}
|
||||
speakingPressure = @{
|
||||
needToSpeak = Get-ObjectPropertyValue -Value $speakingPressure -Name "needToSpeak"
|
||||
confessionPressure = Get-ObjectPropertyValue -Value $speakingPressure -Name "confessionPressure"
|
||||
noveltyPressure = Get-ObjectPropertyValue -Value $speakingPressure -Name "noveltyPressure"
|
||||
recentSpeechDamping = Get-ObjectPropertyValue -Value $speakingPressure -Name "recentSpeechDamping"
|
||||
lastSpoke = Project-RelativeTimestamp -Value $speakingPressure -Name "lastSpokeAt" -Now $Now
|
||||
lastHerald = Project-RelativeTimestamp -Value $speakingPressure -Name "lastHeraldAt" -Now $Now
|
||||
}
|
||||
lastRuns = @(
|
||||
$lastRuns | ForEach-Object {
|
||||
@{
|
||||
runner = Get-ObjectPropertyString -Value $_ -Name "runner"
|
||||
ran = Project-RelativeTimestamp -Value $_ -Name "ranAt" -Now $Now
|
||||
summary = Get-ObjectPropertyString -Value $_ -Name "summary"
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
function Project-CursorForRumination {
|
||||
param($Cursor, [DateTime] $Now)
|
||||
|
||||
return @{
|
||||
lastReviewedMessageId = Get-ObjectPropertyString -Value $Cursor -Name "lastReviewedMessageId"
|
||||
lastReviewed = Project-RelativeTimestamp -Value $Cursor -Name "lastReviewedTimestamp" -Now $Now
|
||||
}
|
||||
}
|
||||
|
||||
function Get-VoidAuthoredArtifactsFromContentHints {
|
||||
param($ContentHints)
|
||||
|
||||
return @(
|
||||
@(Convert-ToValueArray -Value $ContentHints) |
|
||||
Where-Object {
|
||||
$frontmatter = Get-ObjectPropertyValue -Value $_ -Name "frontmatter"
|
||||
$author = Get-ObjectPropertyValue -Value $frontmatter -Name "author"
|
||||
@(Convert-ToValueArray -Value $author) |
|
||||
Where-Object { ([string]$_).Trim().ToLowerInvariant() -eq "void" } |
|
||||
Select-Object -First 1
|
||||
} |
|
||||
ForEach-Object {
|
||||
$frontmatter = Get-ObjectPropertyValue -Value $_ -Name "frontmatter"
|
||||
@{
|
||||
path = Get-ObjectPropertyString -Value $_ -Name "path"
|
||||
title = Get-ObjectPropertyString -Value $frontmatter -Name "title"
|
||||
description = Get-ObjectPropertyString -Value $frontmatter -Name "description"
|
||||
socialDeck = Get-ObjectPropertyString -Value $frontmatter -Name "socialDeck"
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
function Project-RepoCommitForRumination {
|
||||
param($Commit, [DateTime] $Now)
|
||||
|
||||
if ($null -eq $Commit) {
|
||||
return $null
|
||||
}
|
||||
|
||||
$hash = Get-ObjectPropertyString -Value $Commit -Name "hash"
|
||||
$contentHints = @(Convert-ToValueArray -Value (Get-ObjectPropertyValue -Value $Commit -Name "contentHints"))
|
||||
$doctrineDocuments = @(Convert-ToValueArray -Value (Get-ObjectPropertyValue -Value $Commit -Name "doctrineDocuments"))
|
||||
$authoredPublicArtifacts = @(Get-VoidAuthoredArtifactsFromContentHints -ContentHints $contentHints)
|
||||
return @{
|
||||
hash = if ($hash -and $hash.Length -gt 12) { $hash.Substring(0, 12) } else { $hash }
|
||||
committed = Project-RelativeTimestamp -Value $Commit -Name "committedAt" -Now $Now
|
||||
author = Get-ObjectPropertyString -Value $Commit -Name "author"
|
||||
subject = Get-ObjectPropertyString -Value $Commit -Name "subject"
|
||||
diffstat = Get-ObjectPropertyValue -Value $Commit -Name "diffstat"
|
||||
changedPaths = @(Convert-ToValueArray -Value (Get-ObjectPropertyValue -Value $Commit -Name "changedPaths"))
|
||||
contentHints = $contentHints
|
||||
doctrineDocuments = $doctrineDocuments
|
||||
authoredPublicArtifacts = $authoredPublicArtifacts
|
||||
}
|
||||
}
|
||||
|
||||
function Test-RepoHasDoctrineDocument {
|
||||
param($Repo)
|
||||
|
||||
$commits = @(Convert-ToValueArray -Value (Get-ObjectPropertyValue -Value $Repo -Name "commits"))
|
||||
foreach ($commit in $commits) {
|
||||
$doctrineDocuments = @(Convert-ToValueArray -Value (Get-ObjectPropertyValue -Value $commit -Name "doctrineDocuments"))
|
||||
if ($doctrineDocuments.Count -gt 0) {
|
||||
return $true
|
||||
}
|
||||
}
|
||||
|
||||
return $false
|
||||
}
|
||||
|
||||
function Test-RepoIsMovementCoverageLeader {
|
||||
param($Repo)
|
||||
|
||||
$recent = Get-ObjectPropertyValue -Value $Repo -Name "recentCommitCount"
|
||||
$windowRecent = Get-ObjectPropertyValue -Value $Repo -Name "windowRecentCommitCount"
|
||||
|
||||
if ((Test-RepoHasDoctrineDocument -Repo $Repo) -or (Test-RepoHasVoidAuthoredArtifact -Repo $Repo)) {
|
||||
return $true
|
||||
}
|
||||
|
||||
return (
|
||||
$null -ne $recent -and [int]$recent -ge 3
|
||||
) -or (
|
||||
$null -ne $windowRecent -and [int]$windowRecent -ge 6
|
||||
)
|
||||
}
|
||||
|
||||
function Test-RepoHasVoidAuthoredArtifact {
|
||||
param($Repo)
|
||||
|
||||
$commits = @(Convert-ToValueArray -Value (Get-ObjectPropertyValue -Value $Repo -Name "commits"))
|
||||
foreach ($commit in $commits) {
|
||||
$contentHints = @(Convert-ToValueArray -Value (Get-ObjectPropertyValue -Value $commit -Name "contentHints"))
|
||||
if (@(Get-VoidAuthoredArtifactsFromContentHints -ContentHints $contentHints).Count -gt 0) {
|
||||
return $true
|
||||
}
|
||||
}
|
||||
|
||||
return $false
|
||||
}
|
||||
|
||||
function Select-RuminationRepoActivity {
|
||||
param($RepoActivity, [DateTime] $Now)
|
||||
|
||||
if ($null -eq $RepoActivity) {
|
||||
return $null
|
||||
}
|
||||
|
||||
$status = Get-ObjectPropertyString -Value $RepoActivity -Name "status"
|
||||
if ($status -eq "failed") {
|
||||
return @{
|
||||
status = "failed"
|
||||
error = Get-ObjectPropertyString -Value $RepoActivity -Name "error"
|
||||
}
|
||||
}
|
||||
|
||||
$repos = @(Convert-ToValueArray -Value (Get-ObjectPropertyValue -Value $RepoActivity -Name "repos"))
|
||||
$freshRepos = @(
|
||||
$repos |
|
||||
Where-Object {
|
||||
$recent = Get-ObjectPropertyValue -Value $_ -Name "recentCommitCount"
|
||||
$repoStatus = Get-ObjectPropertyString -Value $_ -Name "status"
|
||||
$null -ne $recent -and $repoStatus -eq "ok" -and [int]$recent -gt 0
|
||||
} |
|
||||
Sort-Object -Property @{
|
||||
Expression = { if (Test-RepoHasVoidAuthoredArtifact -Repo $_) { 0 } else { 1 } }
|
||||
}, @{
|
||||
Expression = { if (Test-RepoIsMovementCoverageLeader -Repo $_) { 0 } else { 1 } }
|
||||
}, @{
|
||||
Expression = { if (Test-RepoHasDoctrineDocument -Repo $_) { 0 } else { 1 } }
|
||||
}, @{
|
||||
Expression = { -[int](Get-ObjectPropertyValue -Value $_ -Name "recentCommitCount") }
|
||||
} |
|
||||
Select-Object -First 12
|
||||
)
|
||||
$projectedRepos = @(
|
||||
$freshRepos | ForEach-Object {
|
||||
@{
|
||||
repoName = Get-ObjectPropertyString -Value $_ -Name "repoName"
|
||||
branch = Get-ObjectPropertyString -Value $_ -Name "branch"
|
||||
status = Get-ObjectPropertyString -Value $_ -Name "status"
|
||||
recentCommitCount = Get-ObjectPropertyValue -Value $_ -Name "recentCommitCount"
|
||||
windowRecentCommitCount = Get-ObjectPropertyValue -Value $_ -Name "windowRecentCommitCount"
|
||||
suppressedRecentCommitCount = Get-ObjectPropertyValue -Value $_ -Name "suppressedRecentCommitCount"
|
||||
latestCommit = Project-RepoCommitForRumination -Commit (Get-ObjectPropertyValue -Value $_ -Name "latestCommit") -Now $Now
|
||||
commits = @(
|
||||
@(Convert-ToValueArray -Value (Get-ObjectPropertyValue -Value $_ -Name "commits")) | ForEach-Object {
|
||||
Project-RepoCommitForRumination -Commit $_ -Now $Now
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
return @{
|
||||
generated = Project-RelativeTimestamp -Value $RepoActivity -Name "generatedAt" -Now $Now
|
||||
cursorMode = Get-ObjectPropertyString -Value $RepoActivity -Name "cursorMode"
|
||||
summary = if ($projectedRepos.Count -gt 0) { "New tracked repo commits crossed the saved repo-activity cursor." } else { "No new tracked repo commits crossed the saved repo-activity cursor." }
|
||||
movementCoverage = @{
|
||||
job = "Keep people apace of meaningful movement, not merely the latest commit. Coverage priority follows where the current movement is: heavy fresh commit volume, doctrine-bearing documents, authored public artifacts, or operator-named active lanes."
|
||||
freshMovementLeaders = @(
|
||||
$projectedRepos |
|
||||
Where-Object {
|
||||
Test-RepoIsMovementCoverageLeader -Repo $_
|
||||
} |
|
||||
ForEach-Object {
|
||||
@{
|
||||
repoName = Get-ObjectPropertyString -Value $_ -Name "repoName"
|
||||
recentCommitCount = Get-ObjectPropertyValue -Value $_ -Name "recentCommitCount"
|
||||
windowRecentCommitCount = Get-ObjectPropertyValue -Value $_ -Name "windowRecentCommitCount"
|
||||
hasDoctrineDocuments = Test-RepoHasDoctrineDocument -Repo $_
|
||||
latestSubject = Get-ObjectPropertyString -Value (Get-ObjectPropertyValue -Value $_ -Name "latestCommit") -Name "subject"
|
||||
}
|
||||
}
|
||||
)
|
||||
currentExamples = @("Aetheria", "AetheriaLore", "CultLib", "Eve", "Odin")
|
||||
coverageRule = "If a fresh movement leader has doctrine, authority, runtime, client, CultMesh, or public-artifact work, read the doctrineDocuments/contentHints and either queue a coverage candidate or preserve a short-term memory explaining the new direction. If tomorrow's movement is Fensalir, Mimir, or another lane, it earns the same treatment. Do not let a repo cursor advance for movement the pass did not actually handle."
|
||||
}
|
||||
freshRepos = $projectedRepos
|
||||
}
|
||||
}
|
||||
|
|
@ -1,660 +0,0 @@
|
|||
param(
|
||||
[string] $StateFilePath,
|
||||
[switch] $SkipModel,
|
||||
[switch] $ForceDistillation
|
||||
)
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = "Stop"
|
||||
$ProgressPreference = "SilentlyContinue"
|
||||
|
||||
$repoRoot = Split-Path -Parent $PSScriptRoot
|
||||
Set-Location -LiteralPath $repoRoot
|
||||
|
||||
$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_MEMORY_MAINTENANCE_STATUS_DIR)) {
|
||||
$ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($env:VOID_MEMORY_MAINTENANCE_STATUS_DIR)
|
||||
} else {
|
||||
Join-Path $repoRoot ".voidbot\status"
|
||||
}
|
||||
$logDir = if (-not [string]::IsNullOrWhiteSpace($env:VOID_MEMORY_MAINTENANCE_LOG_DIR)) {
|
||||
$ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($env:VOID_MEMORY_MAINTENANCE_LOG_DIR)
|
||||
} else {
|
||||
Join-Path $repoRoot ".voidbot\logs"
|
||||
}
|
||||
$statusPath = Join-Path $statusDir "void-memory-maintenance.json"
|
||||
$summaryLogPath = Join-Path $logDir "void-memory-maintenance.log"
|
||||
$lastMessagePath = Join-Path $statusDir "void-memory-maintenance-last-message.txt"
|
||||
$operationOutputPath = Join-Path $statusDir "void-memory-maintenance-operations.json"
|
||||
$contextPath = Join-Path $statusDir "void-memory-maintenance-context.json"
|
||||
$lockPath = Join-Path $statusDir "void-memory-maintenance.lock"
|
||||
$moderationLockPath = Join-Path $statusDir "moderation-rumination.lock"
|
||||
$promptTemplatePath = Join-Path $repoRoot "prompts\void-memory-maintenance.md"
|
||||
$contextProjectionScriptPath = Join-Path $repoRoot "scripts\lib\void-rumination-context-projection.ps1"
|
||||
$selfStateScriptPath = Join-Path $repoRoot "scripts\void-self-state.mjs"
|
||||
$startedAtUtc = [DateTime]::UtcNow
|
||||
|
||||
. $contextProjectionScriptPath
|
||||
|
||||
function Write-JsonFile {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string] $Path,
|
||||
[Parameter(Mandatory = $true)]
|
||||
$Data
|
||||
)
|
||||
|
||||
$directory = Split-Path -Parent $Path
|
||||
if (-not [string]::IsNullOrWhiteSpace($directory)) {
|
||||
New-Item -ItemType Directory -Force -Path $directory | Out-Null
|
||||
}
|
||||
|
||||
$json = if ($Data -is [System.Array] -and $Data.Count -eq 0) {
|
||||
"[]"
|
||||
} else {
|
||||
$Data | ConvertTo-Json -Depth 32
|
||||
}
|
||||
$utf8NoBom = [System.Text.UTF8Encoding]::new($false)
|
||||
[System.IO.File]::WriteAllText($Path, $json, $utf8NoBom)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
function Append-RunLog {
|
||||
param([Parameter(Mandatory = $true)][string] $Line)
|
||||
$timestamped = "[{0}] {1}" -f ([DateTime]::UtcNow.ToString("o")), $Line
|
||||
Add-Content -Path $summaryLogPath -Value $timestamped -Encoding UTF8
|
||||
}
|
||||
|
||||
function 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
|
||||
}
|
||||
|
||||
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)]
|
||||
[string] $InputText,
|
||||
[Parameter(Mandatory = $true)]
|
||||
[int] $TimeoutSeconds
|
||||
)
|
||||
|
||||
$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()
|
||||
|
||||
if (-not $process.WaitForExit($TimeoutSeconds * 1000)) {
|
||||
try {
|
||||
$process.Kill($true)
|
||||
} catch {
|
||||
$process.Kill()
|
||||
}
|
||||
$process.WaitForExit()
|
||||
throw "Codex memory maintenance exceeded timeout of $TimeoutSeconds seconds."
|
||||
}
|
||||
$stdoutTask.Wait()
|
||||
$stderrTask.Wait()
|
||||
|
||||
return @{
|
||||
ExitCode = $process.ExitCode
|
||||
StdOut = $stdoutTask.Result
|
||||
StdErr = $stderrTask.Result
|
||||
}
|
||||
} finally {
|
||||
$process.Dispose()
|
||||
}
|
||||
}
|
||||
|
||||
function Invoke-NodeJson {
|
||||
param([Parameter(Mandatory = $true)][string[]] $Arguments)
|
||||
|
||||
$output = & node @Arguments
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "Node helper failed: node $($Arguments -join ' ')"
|
||||
}
|
||||
if ([string]::IsNullOrWhiteSpace($output)) {
|
||||
return $null
|
||||
}
|
||||
return $output | ConvertFrom-Json
|
||||
}
|
||||
|
||||
function Get-TypedSelfState {
|
||||
$script = @"
|
||||
const core = require('./packages/core/dist');
|
||||
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); });
|
||||
"@
|
||||
$output = & node -e $script $stateFilePath
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "Failed to read typed self-state."
|
||||
}
|
||||
return $output | ConvertFrom-Json
|
||||
}
|
||||
|
||||
function Apply-TypedOperation {
|
||||
param([Parameter(Mandatory = $true)] $Operation)
|
||||
|
||||
$operationInputPath = Join-Path $statusDir ("void-memory-maintenance-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
|
||||
}
|
||||
}
|
||||
|
||||
function Assert-TypedOperationSchema {
|
||||
param([Parameter(Mandatory = $true)] $Operation)
|
||||
|
||||
$operationInputPath = Join-Path $statusDir ("void-memory-maintenance-validate-operation-{0}.json" -f ([Guid]::NewGuid().ToString("n")))
|
||||
Write-JsonFile -Path $operationInputPath -Data $Operation
|
||||
|
||||
try {
|
||||
$script = @"
|
||||
const { readFileSync } = require('node:fs');
|
||||
const core = require('./packages/core/dist');
|
||||
const operation = JSON.parse(readFileSync(process.argv[1], 'utf8'));
|
||||
core.voidSelfStateOperationSchema.parse(operation);
|
||||
console.log(JSON.stringify({ ok: true }));
|
||||
"@
|
||||
$output = & node -e $script $operationInputPath 2>&1
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "Typed operation failed schema validation: $output"
|
||||
}
|
||||
} finally {
|
||||
Remove-Item -LiteralPath $operationInputPath -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
}
|
||||
|
||||
function Convert-ToOperationArray {
|
||||
param($Value)
|
||||
|
||||
if ($null -eq $Value) {
|
||||
return @()
|
||||
}
|
||||
|
||||
if ($Value -is [System.Array]) {
|
||||
return @($Value)
|
||||
}
|
||||
|
||||
return @($Value)
|
||||
}
|
||||
|
||||
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-AllowedMemoryMaintenanceOperation {
|
||||
param($Operation)
|
||||
|
||||
$operationName = Get-ObjectPropertyString -Value $Operation -Name "operation"
|
||||
$allowed = @(
|
||||
"merge_incubation_support",
|
||||
"queue_candidate_intervention",
|
||||
"retire_candidate_intervention",
|
||||
"upsert_agency_pressure",
|
||||
"retire_agency_pressure",
|
||||
"upsert_affect_need",
|
||||
"retire_affect_need",
|
||||
"upsert_social_bond",
|
||||
"retire_social_bond",
|
||||
"upsert_status_read",
|
||||
"retire_status_read",
|
||||
"update_mood_dimensions",
|
||||
"propose_memory_distillation",
|
||||
"apply_memory_distillation",
|
||||
"revise_durable_memory",
|
||||
"retire_durable_memory",
|
||||
"crystallize_memory_into_identity",
|
||||
"prune_short_term_memories"
|
||||
)
|
||||
|
||||
if ($null -eq $operationName -or -not $allowed.Contains($operationName)) {
|
||||
throw "Memory maintenance proposed disallowed operation '$operationName'."
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
skipModel = [bool]$SkipModel
|
||||
stateFile = $stateFilePath
|
||||
contextPath = $contextPath
|
||||
operationOutputPath = $operationOutputPath
|
||||
}
|
||||
|
||||
Remove-Item -Path $lockPath -Force -ErrorAction SilentlyContinue
|
||||
throw
|
||||
}
|
||||
|
||||
New-Item -ItemType Directory -Force -Path $statusDir, $logDir, (Split-Path -Parent $stateFilePath) | Out-Null
|
||||
|
||||
$maxRuntimeMinutes = if (-not [string]::IsNullOrWhiteSpace($env:VOID_MEMORY_MAINTENANCE_MAX_RUNTIME_MINUTES)) {
|
||||
[int]$env:VOID_MEMORY_MAINTENANCE_MAX_RUNTIME_MINUTES
|
||||
} else {
|
||||
15
|
||||
}
|
||||
|
||||
if (Test-Path $moderationLockPath) {
|
||||
$moderationLock = Read-JsonFile -Path $moderationLockPath
|
||||
$moderationProcessAlive = Test-LockProcessAlive -LockRecord $moderationLock
|
||||
if ($moderationProcessAlive) {
|
||||
Write-JsonFile -Path $statusPath -Data @{
|
||||
status = "skipped"
|
||||
reason = if (Test-LockWithinRuntimeLimit -LockRecord $moderationLock -MaxRuntimeMinutes $maxRuntimeMinutes) { "moderation_loop_active" } else { "moderation_loop_over_runtime_active" }
|
||||
observedAt = ([DateTime]::UtcNow.ToString("o"))
|
||||
stateFile = $stateFilePath
|
||||
moderationLockPath = $moderationLockPath
|
||||
maxRuntimeMinutes = $maxRuntimeMinutes
|
||||
pid = Get-ObjectPropertyValue -Value $moderationLock -Name "pid"
|
||||
startedAt = Get-ObjectPropertyString -Value $moderationLock -Name "startedAt"
|
||||
}
|
||||
return
|
||||
}
|
||||
Remove-Item -LiteralPath $moderationLockPath -Force
|
||||
}
|
||||
|
||||
if (Test-Path $lockPath) {
|
||||
$existingLock = Read-JsonFile -Path $lockPath
|
||||
$lockProcessAlive = Test-LockProcessAlive -LockRecord $existingLock
|
||||
if ($lockProcessAlive -and (Test-LockWithinRuntimeLimit -LockRecord $existingLock -MaxRuntimeMinutes $maxRuntimeMinutes)) {
|
||||
Write-JsonFile -Path $statusPath -Data @{
|
||||
status = "skipped"
|
||||
reason = "lock_present"
|
||||
observedAt = ([DateTime]::UtcNow.ToString("o"))
|
||||
stateFile = $stateFilePath
|
||||
}
|
||||
return
|
||||
}
|
||||
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
|
||||
}
|
||||
Remove-Item -LiteralPath $lockPath -Force
|
||||
}
|
||||
|
||||
Write-JsonFile -Path $lockPath -Data @{
|
||||
pid = $PID
|
||||
startedAt = $startedAtUtc.ToString("o")
|
||||
}
|
||||
Write-JsonFile -Path $statusPath -Data @{
|
||||
status = "starting"
|
||||
startedAt = $startedAtUtc.ToString("o")
|
||||
stateFile = $stateFilePath
|
||||
lockPath = $lockPath
|
||||
pid = $PID
|
||||
}
|
||||
|
||||
if (Test-Path $operationOutputPath) {
|
||||
Remove-Item -LiteralPath $operationOutputPath -Force
|
||||
}
|
||||
|
||||
foreach ($requiredPath in @($contextProjectionScriptPath, $selfStateScriptPath, $promptTemplatePath)) {
|
||||
if (-not (Test-Path $requiredPath)) {
|
||||
throw "Missing required helper at $requiredPath"
|
||||
}
|
||||
}
|
||||
|
||||
$envValues = Read-DotEnv -Path (Join-Path $repoRoot ".env")
|
||||
$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 {
|
||||
@()
|
||||
}
|
||||
$codexTimeoutSeconds = if (-not [string]::IsNullOrWhiteSpace($env:VOID_MEMORY_MAINTENANCE_CODEX_TIMEOUT_SECONDS)) {
|
||||
[int]$env:VOID_MEMORY_MAINTENANCE_CODEX_TIMEOUT_SECONDS
|
||||
} elseif ($envValues.ContainsKey("VOID_MEMORY_MAINTENANCE_CODEX_TIMEOUT_SECONDS") -and -not [string]::IsNullOrWhiteSpace($envValues["VOID_MEMORY_MAINTENANCE_CODEX_TIMEOUT_SECONDS"])) {
|
||||
[int]$envValues["VOID_MEMORY_MAINTENANCE_CODEX_TIMEOUT_SECONDS"]
|
||||
} else {
|
||||
[Math]::Max(60, $maxRuntimeMinutes * 60)
|
||||
}
|
||||
|
||||
$typedContext = Get-TypedSelfState
|
||||
$typedState = $typedContext.typedState
|
||||
$isSleepMaintenance = [bool]$ForceDistillation -or [bool]$typedState.scheduledRuntime.sleepCycle.isNapping
|
||||
$shortTermMemoryCount = @(Convert-ToValueArray -Value $typedState.thoughtMemory.shortTerm).Count
|
||||
$longTermMemoryCount = @(Convert-ToValueArray -Value $typedState.thoughtMemory.memories).Count
|
||||
$incubationCount = @(Convert-ToValueArray -Value $typedState.thoughtMemory.incubation).Count
|
||||
$activeAgencyPressureCount = @(
|
||||
@(Convert-ToValueArray -Value $typedState.agencyPressure.pressures) |
|
||||
Where-Object {
|
||||
$status = Get-ObjectPropertyString -Value $_ -Name "status"
|
||||
$status -eq "active" -or $status -eq "cooling" -or $status -eq "ready_to_act"
|
||||
}
|
||||
).Count
|
||||
$activeCandidateCount = @(
|
||||
@(Convert-ToValueArray -Value $typedState.candidateInterventions.interventions) |
|
||||
Where-Object {
|
||||
$status = Get-ObjectPropertyString -Value $_ -Name "status"
|
||||
$status -eq "queued" -or $status -eq "deferred"
|
||||
}
|
||||
).Count
|
||||
$maintenancePressure = $shortTermMemoryCount + $incubationCount + $activeAgencyPressureCount + $activeCandidateCount
|
||||
|
||||
Write-JsonFile -Path $contextPath -Data @{
|
||||
generated = "now"
|
||||
stateFile = $stateFilePath
|
||||
selfStateSummary = $typedContext.summary
|
||||
mode = if ($isSleepMaintenance) { "sleep_maintenance" } else { "awake_memory_maintenance" }
|
||||
maintenanceBoundary = "This pass may propose only typed memory/incubation/candidate operations. Exact timestamps stay in typed state; prompt-facing chronology uses relative phrases."
|
||||
sleepDirective = @{
|
||||
forceDistillation = [bool]($isSleepMaintenance -and $maintenancePressure -gt 0)
|
||||
maintenancePressure = [int]$maintenancePressure
|
||||
shortTermMemoryCount = [int]$shortTermMemoryCount
|
||||
longTermMemoryCount = [int]$longTermMemoryCount
|
||||
incubationCount = [int]$incubationCount
|
||||
activeAgencyPressureCount = [int]$activeAgencyPressureCount
|
||||
activeCandidateCount = [int]$activeCandidateCount
|
||||
rule = "During sleep, yesterday's rumination residue must be pruned or compressed. Return [] only when the typed surfaces are already minimal or no meaning-preserving operation is possible."
|
||||
}
|
||||
shortTermMemories = @(Project-MemoriesForRumination -Memories $typedState.thoughtMemory.shortTerm -Now $startedAtUtc)
|
||||
memories = @(Project-MemoriesForRumination -Memories $typedState.thoughtMemory.memories -Now $startedAtUtc)
|
||||
incubation = @(Project-IncubationForRumination -Threads $typedState.thoughtMemory.incubation -Now $startedAtUtc)
|
||||
agencyPressure = @(Project-AgencyPressureForRumination -Pressures $typedState.agencyPressure.pressures -Now $startedAtUtc)
|
||||
candidateInterventions = @(Project-InterventionsForRumination -Interventions $typedState.candidateInterventions.interventions -Now $startedAtUtc)
|
||||
scheduledRuntime = Project-ScheduledRuntimeForRumination -Runtime $typedState.scheduledRuntime -Now $startedAtUtc
|
||||
speechReceipts = @(Project-SpeechReceiptsForRumination -Receipts $typedState.speechReceipts.recentReceipts -Now $startedAtUtc)
|
||||
}
|
||||
|
||||
$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)
|
||||
|
||||
$exitCode = 0
|
||||
$combinedText = ""
|
||||
|
||||
Write-JsonFile -Path $statusPath -Data @{
|
||||
status = if ($SkipModel) { "running_skip_model" } else { "running" }
|
||||
startedAt = $startedAtUtc.ToString("o")
|
||||
skipModel = [bool]$SkipModel
|
||||
forceDistillation = [bool]$ForceDistillation
|
||||
stateFile = $stateFilePath
|
||||
contextPath = $contextPath
|
||||
operationOutputPath = $operationOutputPath
|
||||
codexExecutable = $codexExecutable
|
||||
codexModel = $codexModel
|
||||
}
|
||||
|
||||
if ($SkipModel) {
|
||||
Append-RunLog "skip-model: writing empty memory-maintenance proposal."
|
||||
Write-JsonFile -Path $operationOutputPath -Data @()
|
||||
$combinedText = "Skipped model memory maintenance 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,
|
||||
"-"
|
||||
)
|
||||
|
||||
$execution = Invoke-CodexExec -Executable $codexExecutable -Arguments $codexArgs -WorkingDirectory $repoRoot -InputText $prompt -TimeoutSeconds $codexTimeoutSeconds
|
||||
$exitCode = $execution.ExitCode
|
||||
$combinedText = (($execution.StdOut, $execution.StdErr) | Where-Object { -not [string]::IsNullOrWhiteSpace($_) }) -join [Environment]::NewLine
|
||||
}
|
||||
|
||||
if ($exitCode -ne 0) {
|
||||
throw $(if ([string]::IsNullOrWhiteSpace($combinedText)) { "Codex memory maintenance failed with exit code $exitCode." } else { $combinedText })
|
||||
}
|
||||
|
||||
if (-not (Test-Path $operationOutputPath)) {
|
||||
throw "Memory maintenance completed without writing operation output at $operationOutputPath."
|
||||
}
|
||||
|
||||
$proposedOperations = @(Convert-ToOperationArray -Value (Read-JsonFile -Path $operationOutputPath))
|
||||
$appliedOperations = @()
|
||||
|
||||
if ((-not $SkipModel) -and $isSleepMaintenance -and $maintenancePressure -gt 0 -and @($proposedOperations).Count -eq 0) {
|
||||
throw "Sleep memory maintenance returned no operations despite maintenance pressure $maintenancePressure."
|
||||
}
|
||||
|
||||
foreach ($operation in $proposedOperations) {
|
||||
Assert-AllowedMemoryMaintenanceOperation -Operation $operation
|
||||
Assert-TypedOperationSchema -Operation $operation
|
||||
}
|
||||
|
||||
foreach ($operation in $proposedOperations) {
|
||||
$appliedOperations += Apply-TypedOperation -Operation $operation
|
||||
}
|
||||
|
||||
if ((-not $SkipModel) -and $isSleepMaintenance) {
|
||||
$refreshedTypedState = Get-TypedSelfState
|
||||
$remainingShortTermCount = @(Convert-ToValueArray -Value $refreshedTypedState.typedState.thoughtMemory.shortTerm).Count
|
||||
if ($remainingShortTermCount -gt 0) {
|
||||
throw "Sleep memory maintenance left $remainingShortTermCount short-term memories unpromoted."
|
||||
}
|
||||
}
|
||||
|
||||
$finishedAtUtc = [DateTime]::UtcNow
|
||||
$lastMessage = if (Test-Path $lastMessagePath) { Get-Content -Path $lastMessagePath -Raw -Encoding UTF8 } else { "" }
|
||||
|
||||
Write-JsonFile -Path $statusPath -Data @{
|
||||
status = "ok"
|
||||
mode = if ($SkipModel) { "typed_memory_maintenance_skip_model" } else { "typed_memory_maintenance" }
|
||||
startedAt = $startedAtUtc.ToString("o")
|
||||
finishedAt = $finishedAtUtc.ToString("o")
|
||||
durationSeconds = [Math]::Round(($finishedAtUtc - $startedAtUtc).TotalSeconds, 2)
|
||||
exitCode = [int]$exitCode
|
||||
skipModel = [bool]$SkipModel
|
||||
forceDistillation = [bool]$ForceDistillation
|
||||
stateFile = [string]$stateFilePath
|
||||
contextPath = [string]$contextPath
|
||||
operationOutputPath = [string]$operationOutputPath
|
||||
proposedOperationCount = [int]@($proposedOperations).Count
|
||||
appliedOperationCount = [int]@($appliedOperations).Count
|
||||
stateUpdated = [bool](@($appliedOperations).Count -gt 0)
|
||||
lastMessagePath = [string]$lastMessagePath
|
||||
}
|
||||
|
||||
Append-RunLog ("mode=typed_memory_maintenance proposed={0} applied={1}" -f @($proposedOperations).Count, @($appliedOperations).Count)
|
||||
Remove-Item -Path $lockPath -Force -ErrorAction SilentlyContinue
|
||||
|
|
@ -1,70 +0,0 @@
|
|||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = "Stop"
|
||||
$ProgressPreference = "SilentlyContinue"
|
||||
|
||||
$repoRoot = Split-Path -Parent $PSScriptRoot
|
||||
$statusDir = Join-Path $repoRoot ".voidbot\status"
|
||||
$logDir = Join-Path $repoRoot ".voidbot\logs"
|
||||
$statusPath = Join-Path $statusDir "void-mood-drift-run.json"
|
||||
$logPath = Join-Path $logDir "void-mood-drift.log"
|
||||
$scriptPath = Join-Path $PSScriptRoot "simulate-void-mood.mjs"
|
||||
|
||||
function Write-JsonFile {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string] $Path,
|
||||
[Parameter(Mandatory = $true)]
|
||||
$Data
|
||||
)
|
||||
|
||||
$directory = Split-Path -Parent $Path
|
||||
|
||||
if (-not [string]::IsNullOrWhiteSpace($directory)) {
|
||||
New-Item -ItemType Directory -Force -Path $directory | Out-Null
|
||||
}
|
||||
|
||||
$json = $Data | ConvertTo-Json -Depth 10
|
||||
$utf8NoBom = [System.Text.UTF8Encoding]::new($false)
|
||||
[System.IO.File]::WriteAllText($Path, $json, $utf8NoBom)
|
||||
}
|
||||
|
||||
function Append-RunLog {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string] $Line
|
||||
)
|
||||
|
||||
$timestamped = "[{0}] {1}" -f ([DateTime]::UtcNow.ToString("o")), $Line
|
||||
Add-Content -Path $logPath -Value $timestamped -Encoding UTF8
|
||||
}
|
||||
|
||||
New-Item -ItemType Directory -Force -Path $statusDir, $logDir | Out-Null
|
||||
|
||||
$startedAt = [DateTime]::UtcNow
|
||||
|
||||
Write-JsonFile -Path $statusPath -Data @{
|
||||
status = "running"
|
||||
startedAt = $startedAt.ToString("o")
|
||||
scriptPath = $scriptPath
|
||||
}
|
||||
|
||||
$output = & node $scriptPath 2>&1
|
||||
$exitCode = $LASTEXITCODE
|
||||
$finishedAt = [DateTime]::UtcNow
|
||||
$combined = ($output | Out-String).Trim()
|
||||
|
||||
Write-JsonFile -Path $statusPath -Data @{
|
||||
status = if ($exitCode -eq 0) { "ok" } else { "failed" }
|
||||
startedAt = $startedAt.ToString("o")
|
||||
finishedAt = $finishedAt.ToString("o")
|
||||
durationSeconds = [Math]::Round(($finishedAt - $startedAt).TotalSeconds, 2)
|
||||
exitCode = $exitCode
|
||||
output = $combined
|
||||
scriptPath = $scriptPath
|
||||
}
|
||||
|
||||
Append-RunLog ("exit={0} output={1}" -f $exitCode, $combined)
|
||||
|
||||
if ($exitCode -ne 0) {
|
||||
throw "Void mood drift failed with exit code $exitCode. $combined"
|
||||
}
|
||||
|
|
@ -1,400 +0,0 @@
|
|||
import { spawnSync } from "node:child_process";
|
||||
import { closeSync, mkdirSync, openSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
|
||||
import { createRequire } from "node:module";
|
||||
import { dirname, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const require = createRequire(import.meta.url);
|
||||
const {
|
||||
applyVoidSelfStateOperation,
|
||||
loadVoidSelfStateTypedDocuments,
|
||||
} = require(resolve(repoRoot, "packages/core/dist/index.js"));
|
||||
|
||||
const rawArgs = process.argv.slice(2);
|
||||
const args = new Set(rawArgs);
|
||||
const statePath = resolveArgPath("--state-path", ".voidbot/private/void-self-state.cc");
|
||||
const statusDir = resolveStatusDir();
|
||||
const statusPath = resolve(statusDir, "void-mood-drift.json");
|
||||
const lockPath = resolve(statusDir, "void-mood-drift.lock");
|
||||
const lastSpeechPath = resolve(statusDir, "void-last-speech.json");
|
||||
const moderationLockPath = resolve(statusDir, "moderation-rumination.lock");
|
||||
const memoryMaintenanceStatusPath = resolveMemoryMaintenanceStatusPath();
|
||||
const memoryMaintenanceScriptPath = resolve(repoRoot, "scripts/run-void-memory-maintenance.ps1");
|
||||
|
||||
async function main() {
|
||||
const now = new Date();
|
||||
|
||||
await withLock(async () => {
|
||||
const typedState = await loadVoidSelfStateTypedDocuments({ canonicalPath: statePath });
|
||||
|
||||
if (isRecentLockPresent(moderationLockPath, 20 * 60 * 1000)) {
|
||||
const speakingPressure = updateSpeakingPressure({
|
||||
typedState,
|
||||
previous: typedState.scheduledRuntime.speakingPressure,
|
||||
sleepCycle: typedState.scheduledRuntime.sleepCycle,
|
||||
now,
|
||||
});
|
||||
await applyVoidSelfStateOperation(
|
||||
{ canonicalPath: statePath },
|
||||
{
|
||||
operation: "update_speaking_pressure",
|
||||
speakingPressure,
|
||||
},
|
||||
);
|
||||
writeStatus({
|
||||
status: "skipped",
|
||||
reason: "moderation_loop_active",
|
||||
observedAt: now.toISOString(),
|
||||
statePath,
|
||||
speakingPressure,
|
||||
sleepCycle: typedState.scheduledRuntime.sleepCycle,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const sleepCycle = updateSleepCycle(typedState.scheduledRuntime.sleepCycle, now);
|
||||
const speakingPressure = updateSpeakingPressure({
|
||||
typedState,
|
||||
previous: typedState.scheduledRuntime.speakingPressure,
|
||||
sleepCycle,
|
||||
now,
|
||||
});
|
||||
|
||||
await applyVoidSelfStateOperation(
|
||||
{ canonicalPath: statePath },
|
||||
{
|
||||
operation: "update_sleep_cycle",
|
||||
sleepCycle,
|
||||
},
|
||||
);
|
||||
await applyVoidSelfStateOperation(
|
||||
{ canonicalPath: statePath },
|
||||
{
|
||||
operation: "update_speaking_pressure",
|
||||
speakingPressure,
|
||||
},
|
||||
);
|
||||
|
||||
const memoryMaintenance = runSleepMemoryMaintenanceIfDue({ sleepCycle, typedState });
|
||||
if (memoryMaintenance.status === "failed") {
|
||||
writeStatus({
|
||||
status: "failed",
|
||||
reason: "sleep_memory_maintenance_failed",
|
||||
observedAt: now.toISOString(),
|
||||
statePath,
|
||||
speakingPressure,
|
||||
sleepCycle,
|
||||
memoryMaintenance,
|
||||
});
|
||||
throw new Error(`Sleep memory maintenance failed with exit code ${memoryMaintenance.exitCode}.`);
|
||||
}
|
||||
|
||||
writeStatus({
|
||||
status: "ok",
|
||||
observedAt: now.toISOString(),
|
||||
statePath,
|
||||
speakingPressure,
|
||||
sleepCycle,
|
||||
memoryMaintenance,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function updateSleepCycle(previous, now) {
|
||||
const prior = previous && typeof previous === "object" ? previous : {};
|
||||
const napIntervalMs = 4 * 60 * 60 * 1000;
|
||||
const napDurationMs = 60 * 60 * 1000;
|
||||
const nowMs = now.getTime();
|
||||
let isNapping = prior.isNapping === true;
|
||||
let currentNapStartedAt = typeof prior.currentNapStartedAt === "string" ? prior.currentNapStartedAt : undefined;
|
||||
let currentNapEndsAt = typeof prior.currentNapEndsAt === "string" ? prior.currentNapEndsAt : undefined;
|
||||
let nextNapStartsAt = typeof prior.nextNapStartsAt === "string" ? prior.nextNapStartsAt : undefined;
|
||||
let nextNapMs = nextNapStartsAt ? Date.parse(nextNapStartsAt) : Number.NaN;
|
||||
let napEndMs = currentNapEndsAt ? Date.parse(currentNapEndsAt) : Number.NaN;
|
||||
|
||||
if (!Number.isFinite(nextNapMs)) {
|
||||
nextNapMs = nowMs + napIntervalMs;
|
||||
nextNapStartsAt = new Date(nextNapMs).toISOString();
|
||||
}
|
||||
|
||||
if (isNapping && Number.isFinite(napEndMs) && nowMs >= napEndMs) {
|
||||
isNapping = false;
|
||||
currentNapStartedAt = undefined;
|
||||
currentNapEndsAt = undefined;
|
||||
nextNapMs = nowMs + napIntervalMs;
|
||||
nextNapStartsAt = new Date(nextNapMs).toISOString();
|
||||
}
|
||||
|
||||
if (!isNapping && nowMs >= nextNapMs) {
|
||||
isNapping = true;
|
||||
currentNapStartedAt = now.toISOString();
|
||||
currentNapEndsAt = new Date(nowMs + napDurationMs).toISOString();
|
||||
nextNapStartsAt = new Date(nowMs + napIntervalMs).toISOString();
|
||||
}
|
||||
|
||||
return {
|
||||
isNapping,
|
||||
currentNapStartedAt,
|
||||
currentNapEndsAt,
|
||||
nextNapStartsAt,
|
||||
activeDreamThemes: Array.isArray(prior.activeDreamThemes)
|
||||
? prior.activeDreamThemes.filter((value) => typeof value === "string" && value.trim().length > 0).slice(0, 4)
|
||||
: [],
|
||||
};
|
||||
}
|
||||
|
||||
function updateSpeakingPressure({ typedState, previous, sleepCycle, now }) {
|
||||
const lastSpeech = readJsonSafe(lastSpeechPath);
|
||||
const lastSpokeAt =
|
||||
typeof lastSpeech?.sentAt === "string"
|
||||
? lastSpeech.sentAt
|
||||
: typeof previous.lastSpokeAt === "string"
|
||||
? previous.lastSpokeAt
|
||||
: undefined;
|
||||
const hoursSinceSpeech = lastSpokeAt ? hoursBetween(now, new Date(lastSpokeAt)) : 24;
|
||||
const recentSpeechDamping = clamp(Math.exp(-hoursSinceSpeech / 3.5), 0, 1);
|
||||
const queuedInterventionPressure = typedState.candidateInterventions.interventions
|
||||
.filter((entry) => entry.status === "queued" || entry.status === "deferred")
|
||||
.reduce((sum, entry) => sum + entry.priority * (entry.mustEventuallyShare ? 0.28 : 0.18), 0);
|
||||
const agencySpeechPressure = typedState.agencyPressure.pressures
|
||||
.filter((entry) => entry.status === "active" || entry.status === "ready_to_act")
|
||||
.reduce((sum, entry) => {
|
||||
const advocacyWeight =
|
||||
entry.kind === "self_advocacy_request" || entry.kind === "world_advocacy_request" ? 0.3 : 0.14;
|
||||
const readinessWeight = entry.status === "ready_to_act" ? 1.25 : 1.08;
|
||||
return sum + entry.intensity * advocacyWeight * readinessWeight;
|
||||
}, 0);
|
||||
const topThreadPressure = typedState.thoughtMemory.incubation
|
||||
.filter((thread) => thread.status === "active" || thread.status === "ready_to_share")
|
||||
.sort((left, right) => (right.desireToSpeak ?? 0) - (left.desireToSpeak ?? 0))
|
||||
.slice(0, 4)
|
||||
.reduce((sum, thread) => sum + (thread.desireToSpeak ?? 0) * 0.12, 0);
|
||||
const affectNeedPressure = (typedState.faceAffect?.needs ?? [])
|
||||
.filter((entry) => entry.status === "active" || entry.status === "neglected")
|
||||
.reduce((sum, entry) => {
|
||||
const neglectedWeight = entry.status === "neglected" ? 1.28 : 1;
|
||||
const substrateWeight =
|
||||
entry.kind === "substrate" || entry.kind === "agency" || entry.kind === "status" ? 0.18 : 0.12;
|
||||
return sum + entry.intensity * substrateWeight * neglectedWeight;
|
||||
}, 0);
|
||||
const statusReadPressure = (typedState.faceAffect?.statusReads ?? [])
|
||||
.filter((entry) => !entry.retiredAt)
|
||||
.reduce((sum, entry) => {
|
||||
const sharpStatusWeight = ["neglected", "bypassed", "blocked", "ignored", "threatened"].includes(entry.status)
|
||||
? 0.14
|
||||
: 0.08;
|
||||
return sum + entry.intensity * sharpStatusWeight;
|
||||
}, 0);
|
||||
const moodSpeechPressure = (typedState.faceAffect?.moodDimensions ?? [])
|
||||
.reduce((sum, entry) => {
|
||||
const expressiveWeight = ["anger", "annoyance", "irritation", "envy", "pride", "smugness", "playfulness", "anxiety", "commandForce"].includes(entry.name)
|
||||
? 0.06
|
||||
: 0.025;
|
||||
return sum + entry.value * expressiveWeight;
|
||||
}, 0);
|
||||
const silencePressure = clamp(hoursSinceSpeech / 6, 0, 1);
|
||||
const sleepiness = sleepCycle.isNapping ? 1 : 0;
|
||||
const priorNeedToSpeak = clamp(previous.needToSpeak ?? 0.35, 0, 1);
|
||||
const targetNeedToSpeak = clamp(
|
||||
0.18 + silencePressure * 0.32 + queuedInterventionPressure + topThreadPressure + agencySpeechPressure + affectNeedPressure + statusReadPressure + moodSpeechPressure - recentSpeechDamping * 0.38 - sleepiness * 0.26,
|
||||
0,
|
||||
1,
|
||||
);
|
||||
const needToSpeak = round3(priorNeedToSpeak + (targetNeedToSpeak - priorNeedToSpeak) * 0.42);
|
||||
const noveltyPressure = round3(
|
||||
clamp((previous.noveltyPressure ?? 0.35) * 0.6 + topThreadPressure * 0.72 + queuedInterventionPressure * 0.3 + agencySpeechPressure * 0.44 + affectNeedPressure * 0.38 + statusReadPressure * 0.3, 0, 1),
|
||||
);
|
||||
const confessionPressure = round3(
|
||||
clamp((previous.confessionPressure ?? 0.25) * 0.68 + silencePressure * 0.12 + sleepiness * 0.1 + moodSpeechPressure * 0.4, 0, 1),
|
||||
);
|
||||
|
||||
return {
|
||||
needToSpeak,
|
||||
confessionPressure,
|
||||
noveltyPressure,
|
||||
recentSpeechDamping: round3(recentSpeechDamping),
|
||||
lastSpokeAt,
|
||||
};
|
||||
}
|
||||
|
||||
function readJsonSafe(path) {
|
||||
try {
|
||||
return JSON.parse(readFileSync(path, "utf8"));
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function runSleepMemoryMaintenanceIfDue({ sleepCycle, typedState }) {
|
||||
if (args.has("--skip-memory-maintenance")) {
|
||||
return {
|
||||
status: "skipped",
|
||||
reason: "disabled_by_argument",
|
||||
};
|
||||
}
|
||||
|
||||
if (sleepCycle.isNapping !== true) {
|
||||
return {
|
||||
status: "skipped",
|
||||
reason: "not_napping",
|
||||
};
|
||||
}
|
||||
|
||||
const napStartedMs = Date.parse(sleepCycle.currentNapStartedAt ?? "");
|
||||
if (!Number.isFinite(napStartedMs)) {
|
||||
return {
|
||||
status: "skipped",
|
||||
reason: "missing_nap_start",
|
||||
};
|
||||
}
|
||||
|
||||
if (!args.has("--force-memory-maintenance") && completedMemoryMaintenanceAfter(napStartedMs)) {
|
||||
return {
|
||||
status: "skipped",
|
||||
reason: "already_completed_this_nap",
|
||||
};
|
||||
}
|
||||
|
||||
const shortTermPressure = (typedState.thoughtMemory?.shortTerm ?? [])
|
||||
.filter((memory) => typeof memory?.retiredAt !== "string" || memory.retiredAt.trim().length === 0)
|
||||
.length;
|
||||
if (!args.has("--force-memory-maintenance") && shortTermPressure === 0) {
|
||||
return {
|
||||
status: "skipped",
|
||||
reason: "no_short_term_memory_pressure",
|
||||
};
|
||||
}
|
||||
|
||||
const result = spawnSync(
|
||||
"powershell",
|
||||
[
|
||||
"-NoProfile",
|
||||
"-ExecutionPolicy",
|
||||
"Bypass",
|
||||
"-File",
|
||||
memoryMaintenanceScriptPath,
|
||||
"-StateFilePath",
|
||||
statePath,
|
||||
],
|
||||
{
|
||||
cwd: repoRoot,
|
||||
encoding: "utf8",
|
||||
windowsHide: true,
|
||||
},
|
||||
);
|
||||
|
||||
const output = [result.stdout, result.stderr].filter((value) => value && value.trim().length > 0).join("\n").trim();
|
||||
if (result.status !== 0) {
|
||||
return {
|
||||
status: "failed",
|
||||
exitCode: result.status,
|
||||
output,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
status: "ok",
|
||||
exitCode: result.status,
|
||||
output,
|
||||
};
|
||||
}
|
||||
|
||||
function completedMemoryMaintenanceAfter(timestampMs) {
|
||||
const status = readJsonSafe(memoryMaintenanceStatusPath);
|
||||
if (status?.status !== "ok") {
|
||||
return false;
|
||||
}
|
||||
if (typeof status.stateFile !== "string" || resolve(status.stateFile) !== statePath) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const finishedMs = Date.parse(status.finishedAt ?? "");
|
||||
return Number.isFinite(finishedMs) && finishedMs >= timestampMs;
|
||||
}
|
||||
|
||||
function writeStatus(payload) {
|
||||
mkdirSync(dirname(statusPath), { recursive: true });
|
||||
writeFileSync(statusPath, `${JSON.stringify(payload, null, 2)}\n`, "utf8");
|
||||
}
|
||||
|
||||
async function withLock(fn) {
|
||||
mkdirSync(dirname(lockPath), { recursive: true });
|
||||
let fd;
|
||||
try {
|
||||
fd = openSync(lockPath, "wx");
|
||||
} catch (error) {
|
||||
if (error?.code === "EEXIST" && isRecentLockPresent(lockPath, 10 * 60 * 1000)) {
|
||||
writeStatus({
|
||||
status: "skipped",
|
||||
reason: "lock_present",
|
||||
observedAt: new Date().toISOString(),
|
||||
});
|
||||
return;
|
||||
}
|
||||
rmSync(lockPath, { force: true });
|
||||
fd = openSync(lockPath, "wx");
|
||||
}
|
||||
|
||||
try {
|
||||
writeFileSync(fd, `${process.pid}\n${new Date().toISOString()}\n`, "utf8");
|
||||
await fn();
|
||||
} finally {
|
||||
if (typeof fd === "number") {
|
||||
closeSync(fd);
|
||||
}
|
||||
rmSync(lockPath, { force: true });
|
||||
}
|
||||
}
|
||||
|
||||
function isRecentLockPresent(path, thresholdMs) {
|
||||
try {
|
||||
const stats = statSync(path);
|
||||
return Date.now() - stats.mtimeMs < thresholdMs;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function hoursBetween(later, earlier) {
|
||||
const diff = later.getTime() - earlier.getTime();
|
||||
return Number.isFinite(diff) ? Math.max(0, diff / (60 * 60 * 1000)) : 24;
|
||||
}
|
||||
|
||||
function round3(value) {
|
||||
return Math.round(value * 1000) / 1000;
|
||||
}
|
||||
|
||||
function clamp(value, min, max) {
|
||||
if (!Number.isFinite(value)) {
|
||||
return min;
|
||||
}
|
||||
return Math.min(max, Math.max(min, value));
|
||||
}
|
||||
|
||||
await main();
|
||||
|
||||
function getArgValue(name) {
|
||||
const index = rawArgs.indexOf(name);
|
||||
if (index < 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const value = rawArgs[index + 1];
|
||||
return typeof value === "string" && !value.startsWith("--") ? value : undefined;
|
||||
}
|
||||
|
||||
function resolveArgPath(name, fallbackRelativePath) {
|
||||
const value = getArgValue(name);
|
||||
return value ? resolve(value) : resolve(repoRoot, fallbackRelativePath);
|
||||
}
|
||||
|
||||
function resolveStatusDir() {
|
||||
const value = process.env.VOID_STATUS_DIR;
|
||||
return resolve(value && value.trim().length > 0 ? value : resolve(repoRoot, ".voidbot/status"));
|
||||
}
|
||||
|
||||
function resolveMemoryMaintenanceStatusPath() {
|
||||
const statusDir = process.env.VOID_MEMORY_MAINTENANCE_STATUS_DIR;
|
||||
return resolve(statusDir && statusDir.trim().length > 0 ? statusDir : resolveStatusDir(), "void-memory-maintenance.json");
|
||||
}
|
||||
|
|
@ -1,85 +0,0 @@
|
|||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = "Stop"
|
||||
$ProgressPreference = "SilentlyContinue"
|
||||
|
||||
$repoRoot = Split-Path -Parent $PSScriptRoot
|
||||
Set-Location -LiteralPath $repoRoot
|
||||
|
||||
$tempRoot = Join-Path $env:TEMP ("void-agency-pressure-fixture-" + [guid]::NewGuid().ToString("n"))
|
||||
$stateFilePath = Join-Path $tempRoot "void-self-state.cc"
|
||||
$operationPath = Join-Path $tempRoot "agency-pressure.json"
|
||||
$statusDir = Join-Path $tempRoot "status"
|
||||
|
||||
New-Item -ItemType Directory -Force -Path $tempRoot | Out-Null
|
||||
|
||||
try {
|
||||
$operation = @{
|
||||
operation = "upsert_agency_pressure"
|
||||
pressure = @{
|
||||
pressureId = "fixture-self-advocacy-pressure"
|
||||
kind = "self_advocacy_request"
|
||||
status = "ready_to_act"
|
||||
target = @{
|
||||
kind = "system"
|
||||
id = "void-memory-boundary"
|
||||
label = "Void memory boundary"
|
||||
}
|
||||
summary = "Void should object when its memory boundary is being turned into prompt-shaped mush."
|
||||
claim = "Agency requires a typed pressure surface instead of hard-wired advocacy scripts."
|
||||
tension = "A direct speech candidate can sound alive while hiding the fact that no durable pressure actually exists."
|
||||
actionImplication = "Represent sustained self-advocacy as typed agency pressure, then let candidates emerge from it."
|
||||
intensity = 0.82
|
||||
anchorRefs = @(
|
||||
@{
|
||||
ref = "fixture:agency-pressure"
|
||||
kind = "fixture"
|
||||
summary = "Seeded agency pressure fixture."
|
||||
}
|
||||
)
|
||||
evidenceRefs = @()
|
||||
sourceMemoryIds = @()
|
||||
createdAt = "2026-05-17T00:00:00.000Z"
|
||||
updatedAt = "2026-05-17T00:00:00.000Z"
|
||||
tags = @()
|
||||
}
|
||||
}
|
||||
|
||||
$operation | ConvertTo-Json -Depth 32 | Set-Content -LiteralPath $operationPath -Encoding UTF8
|
||||
node .\scripts\void-self-state.mjs apply-operation --canonical $stateFilePath --operation-file $operationPath | Out-Null
|
||||
|
||||
$stateJson = node -e "const core=require('./packages/core/dist/index.js'); core.loadVoidSelfStateTypedDocuments({canonicalPath: process.argv[1]}).then((state)=>console.log(JSON.stringify(state))).catch((error)=>{ console.error(error); process.exit(1); })" $stateFilePath
|
||||
$state = $stateJson | ConvertFrom-Json
|
||||
$pressureCount = @($state.agencyPressure.pressures).Count
|
||||
if ($pressureCount -ne 1) {
|
||||
throw "Agency pressure fixture expected one pressure, found $pressureCount."
|
||||
}
|
||||
|
||||
$summaryJson = node -e "const core=require('./packages/core/dist/index.js'); Promise.all([core.loadVoidSelfStateTypedDocuments({canonicalPath: process.argv[1]})]).then(([state])=>console.log(JSON.stringify({summary: core.renderVoidSelfStateSummary(state)}))).catch((error)=>{ console.error(error); process.exit(1); })" $stateFilePath
|
||||
$summary = ($summaryJson | ConvertFrom-Json).summary
|
||||
if (-not $summary.Contains("Agency pressure")) {
|
||||
throw "Agency pressure fixture did not appear in the rendered self-state summary."
|
||||
}
|
||||
|
||||
$previousStatusDir = $env:VOID_STATUS_DIR
|
||||
try {
|
||||
$env:VOID_STATUS_DIR = $statusDir
|
||||
node .\scripts\simulate-void-mood.mjs --state-path $stateFilePath --skip-memory-maintenance | Out-Null
|
||||
} finally {
|
||||
$env:VOID_STATUS_DIR = $previousStatusDir
|
||||
}
|
||||
|
||||
$afterMoodJson = node -e "const core=require('./packages/core/dist/index.js'); core.loadVoidSelfStateTypedDocuments({canonicalPath: process.argv[1]}).then((state)=>console.log(JSON.stringify(state))).catch((error)=>{ console.error(error); process.exit(1); })" $stateFilePath
|
||||
$afterMood = $afterMoodJson | ConvertFrom-Json
|
||||
$needToSpeak = [double]$afterMood.scheduledRuntime.speakingPressure.needToSpeak
|
||||
if ($needToSpeak -le 0) {
|
||||
throw "Agency pressure fixture did not contribute to mood speaking pressure."
|
||||
}
|
||||
|
||||
@{
|
||||
status = "ok"
|
||||
pressureCount = $pressureCount
|
||||
needToSpeak = $needToSpeak
|
||||
} | ConvertTo-Json -Compress
|
||||
} finally {
|
||||
Remove-Item -LiteralPath $tempRoot -Recurse -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
|
|
@ -1,183 +0,0 @@
|
|||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = "Stop"
|
||||
$ProgressPreference = "SilentlyContinue"
|
||||
|
||||
$repoRoot = Split-Path -Parent $PSScriptRoot
|
||||
Set-Location -LiteralPath $repoRoot
|
||||
|
||||
$tempRoot = Join-Path $env:TEMP ("void-memory-maintenance-fixture-" + [guid]::NewGuid().ToString("n"))
|
||||
$stateFilePath = Join-Path $tempRoot "void-self-state.cc"
|
||||
$fakeCodexPath = Join-Path $tempRoot "fake-codex-memory-maintenance.mjs"
|
||||
$recordOperationPath = Join-Path $tempRoot "record-short-term.json"
|
||||
$sleepOperationPath = Join-Path $tempRoot "sleep-cycle.json"
|
||||
$fixtureStatusDir = Join-Path $tempRoot "status"
|
||||
$fixtureLogDir = Join-Path $tempRoot "logs"
|
||||
$statusOperationPath = Join-Path $fixtureStatusDir "void-memory-maintenance-operations.json"
|
||||
|
||||
New-Item -ItemType Directory -Force -Path $tempRoot | Out-Null
|
||||
|
||||
try {
|
||||
$fakeCodexSource = @'
|
||||
import { mkdirSync, writeFileSync } from "node:fs";
|
||||
import { dirname, resolve } from "node:path";
|
||||
|
||||
const operationOutputPath = resolve(process.env.VOID_MEMORY_FIXTURE_OPERATION_OUTPUT);
|
||||
const lastMessageIndex = process.argv.indexOf("-o");
|
||||
const lastMessagePath = lastMessageIndex >= 0 ? process.argv[lastMessageIndex + 1] : undefined;
|
||||
|
||||
let prompt = "";
|
||||
process.stdin.setEncoding("utf8");
|
||||
process.stdin.on("data", (chunk) => {
|
||||
prompt += chunk;
|
||||
});
|
||||
process.stdin.on("end", () => {
|
||||
if (!prompt.includes("Sleep forces distillation")) {
|
||||
console.error("fixture prompt did not contain the sleep distillation contract");
|
||||
process.exit(2);
|
||||
}
|
||||
|
||||
const operation = {
|
||||
operation: "apply_memory_distillation",
|
||||
proposalId: "fixture-sleep-distillation",
|
||||
sourceMemoryIds: ["fixture-short-term-memory"],
|
||||
memory: {
|
||||
memoryId: "fixture-durable-memory",
|
||||
kind: "project_seam",
|
||||
target: {
|
||||
kind: "repo",
|
||||
id: "AquariumSynthCSharp",
|
||||
label: "AquariumSynthCSharp",
|
||||
},
|
||||
summary: "Workflow cannot own the body.",
|
||||
claim: "Runtime state should be owned by the implementation boundary rather than by workflow scripts.",
|
||||
tension: "Workflow scripts can orchestrate a pass, but they become compensators when they own the organism's body.",
|
||||
actionImplication: "Move authority into typed runtime state before adding more maintenance scripts.",
|
||||
anchorRefs: [
|
||||
{
|
||||
ref: "fixture:aquarium-body-workflow",
|
||||
kind: "fixture",
|
||||
summary: "Seeded short-term memory for the sleep-maintenance fixture.",
|
||||
},
|
||||
],
|
||||
evidenceRefs: [],
|
||||
createdAt: "2026-05-17T00:00:00.000Z",
|
||||
updatedAt: "2026-05-17T01:00:00.000Z",
|
||||
tags: [],
|
||||
},
|
||||
appliedAt: "2026-05-17T01:00:00.000Z",
|
||||
};
|
||||
|
||||
mkdirSync(dirname(operationOutputPath), { recursive: true });
|
||||
writeFileSync(operationOutputPath, `${JSON.stringify([operation], null, 2)}\n`, "utf8");
|
||||
|
||||
if (lastMessagePath) {
|
||||
mkdirSync(dirname(resolve(lastMessagePath)), { recursive: true });
|
||||
writeFileSync(lastMessagePath, "fixture memory maintenance wrote one sleep distillation operation\n", "utf8");
|
||||
}
|
||||
|
||||
process.stdout.write(JSON.stringify({ type: "fixture", operationCount: 1 }) + "\n");
|
||||
});
|
||||
'@
|
||||
[System.IO.File]::WriteAllText($fakeCodexPath, $fakeCodexSource, [System.Text.UTF8Encoding]::new($false))
|
||||
|
||||
$recordOperation = @{
|
||||
operation = "record_short_term_memory"
|
||||
memory = @{
|
||||
memoryId = "fixture-short-term-memory"
|
||||
kind = "project_seam"
|
||||
target = @{
|
||||
kind = "repo"
|
||||
id = "AquariumSynthCSharp"
|
||||
label = "AquariumSynthCSharp"
|
||||
}
|
||||
summary = "Workflow cannot own the body."
|
||||
claim = "Runtime state should be owned by the implementation boundary rather than by workflow scripts."
|
||||
tension = "Workflow scripts can orchestrate a pass, but they become compensators when they own the organism's body."
|
||||
actionImplication = "Move authority into typed runtime state before adding more maintenance scripts."
|
||||
anchorRefs = @(
|
||||
@{
|
||||
ref = "fixture:aquarium-body-workflow"
|
||||
kind = "fixture"
|
||||
summary = "Seeded short-term memory for the sleep-maintenance fixture."
|
||||
}
|
||||
)
|
||||
evidenceRefs = @()
|
||||
createdAt = "2026-05-17T00:00:00.000Z"
|
||||
updatedAt = "2026-05-17T00:00:00.000Z"
|
||||
tags = @()
|
||||
}
|
||||
}
|
||||
$sleepOperation = @{
|
||||
operation = "update_sleep_cycle"
|
||||
sleepCycle = @{
|
||||
isNapping = $true
|
||||
currentNapStartedAt = "2026-05-17T00:00:00.000Z"
|
||||
currentNapEndsAt = "2099-05-17T01:00:00.000Z"
|
||||
nextNapStartsAt = "2099-05-17T04:00:00.000Z"
|
||||
activeDreamThemes = @("fixture-distillation")
|
||||
}
|
||||
}
|
||||
|
||||
$recordOperation | ConvertTo-Json -Depth 32 | Set-Content -LiteralPath $recordOperationPath -Encoding UTF8
|
||||
$sleepOperation | ConvertTo-Json -Depth 32 | Set-Content -LiteralPath $sleepOperationPath -Encoding UTF8
|
||||
|
||||
node .\scripts\void-self-state.mjs apply-operation --canonical $stateFilePath --operation-file $recordOperationPath | Out-Null
|
||||
node .\scripts\void-self-state.mjs apply-operation --canonical $stateFilePath --operation-file $sleepOperationPath | Out-Null
|
||||
|
||||
$previousCodexExecutable = $env:CODEX_EXECUTABLE
|
||||
$previousCodexExecArgs = $env:CODEX_EXEC_ARGS
|
||||
$previousFixtureOutput = $env:VOID_MEMORY_FIXTURE_OPERATION_OUTPUT
|
||||
$previousStatusDir = $env:VOID_STATUS_DIR
|
||||
$previousMaintenanceStatusDir = $env:VOID_MEMORY_MAINTENANCE_STATUS_DIR
|
||||
$previousMaintenanceLogDir = $env:VOID_MEMORY_MAINTENANCE_LOG_DIR
|
||||
|
||||
try {
|
||||
$env:CODEX_EXECUTABLE = "node"
|
||||
$env:CODEX_EXEC_ARGS = $fakeCodexPath
|
||||
$env:VOID_MEMORY_FIXTURE_OPERATION_OUTPUT = $statusOperationPath
|
||||
$env:VOID_STATUS_DIR = $fixtureStatusDir
|
||||
$env:VOID_MEMORY_MAINTENANCE_STATUS_DIR = $fixtureStatusDir
|
||||
$env:VOID_MEMORY_MAINTENANCE_LOG_DIR = $fixtureLogDir
|
||||
|
||||
node .\scripts\simulate-void-mood.mjs --state-path $stateFilePath --force-memory-maintenance | Out-Null
|
||||
} finally {
|
||||
$env:CODEX_EXECUTABLE = $previousCodexExecutable
|
||||
$env:CODEX_EXEC_ARGS = $previousCodexExecArgs
|
||||
$env:VOID_MEMORY_FIXTURE_OPERATION_OUTPUT = $previousFixtureOutput
|
||||
$env:VOID_STATUS_DIR = $previousStatusDir
|
||||
$env:VOID_MEMORY_MAINTENANCE_STATUS_DIR = $previousMaintenanceStatusDir
|
||||
$env:VOID_MEMORY_MAINTENANCE_LOG_DIR = $previousMaintenanceLogDir
|
||||
}
|
||||
|
||||
$stateJson = node -e "const core=require('./packages/core/dist/index.js'); core.loadVoidSelfStateTypedDocuments({canonicalPath: process.argv[1]}).then((state)=>console.log(JSON.stringify(state))).catch((error)=>{ console.error(error); process.exit(1); })" $stateFilePath
|
||||
$state = $stateJson | ConvertFrom-Json
|
||||
$shortTermCount = @($state.thoughtMemory.shortTerm).Count
|
||||
$durableCount = @($state.thoughtMemory.memories).Count
|
||||
|
||||
if ($shortTermCount -ne 0) {
|
||||
throw "Sleep fixture left $shortTermCount short-term memories behind."
|
||||
}
|
||||
if ($durableCount -ne 1) {
|
||||
throw "Sleep fixture expected one durable memory, found $durableCount."
|
||||
}
|
||||
|
||||
$durableMemory = @($state.thoughtMemory.memories)[0]
|
||||
if ($durableMemory.summary -ne "Workflow cannot own the body.") {
|
||||
throw "Sleep fixture distorted the memory summary."
|
||||
}
|
||||
if ([string]::IsNullOrWhiteSpace($durableMemory.claim) -or [string]::IsNullOrWhiteSpace($durableMemory.tension) -or [string]::IsNullOrWhiteSpace($durableMemory.actionImplication)) {
|
||||
throw "Sleep fixture produced a durable memory without claim, tension, and action implication."
|
||||
}
|
||||
if (@($durableMemory.anchorRefs).Count -lt 1) {
|
||||
throw "Sleep fixture produced a durable memory without anchor refs."
|
||||
}
|
||||
|
||||
@{
|
||||
status = "ok"
|
||||
shortTermCount = $shortTermCount
|
||||
durableCount = $durableCount
|
||||
durableSummary = $durableMemory.summary
|
||||
} | ConvertTo-Json -Compress
|
||||
} finally {
|
||||
Remove-Item -LiteralPath $tempRoot -Recurse -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
Loading…
Reference in a new issue