From f9ff9d2cc99c0a09fe050505a0c0170be7a4a071 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Tue, 5 May 2026 11:54:01 +0100 Subject: [PATCH] Fix moderation history time handling --- scripts/export-random-discord-history.mjs | 47 ++++++++++++- scripts/export-recent-discord-history.mjs | 46 ++++++++++-- scripts/run-void-moderator-rumination.ps1 | 85 ++++++++++++++++++++++- scripts/run-void-mood-drift.ps1 | 4 +- state/evidence.jsonl | 1 + 5 files changed, 169 insertions(+), 14 deletions(-) diff --git a/scripts/export-random-discord-history.mjs b/scripts/export-random-discord-history.mjs index 300512e..2f9518d 100644 --- a/scripts/export-random-discord-history.mjs +++ b/scripts/export-random-discord-history.mjs @@ -201,16 +201,33 @@ function readArchiveStore(archivePath) { } function filterMessages(messages, options) { + const beforeMs = options.before !== undefined ? parseTimestampToMs(options.before) : undefined; + const afterMs = options.after !== undefined ? parseTimestampToMs(options.after) : undefined; + return messages .filter((message) => !message.deletedAt) .filter((message) => options.includeBotPrompts ? true : readMessageKind(message) !== "bot_prompt", ) .filter((message) => (options.channelId ? message.channelId === options.channelId : true)) - .filter((message) => (options.before ? message.timestamp < options.before : true)) - .filter((message) => (options.after ? message.timestamp > options.after : true)) + .filter((message) => { + if (beforeMs === undefined) { + return true; + } + + const messageMs = parseTimestampToMs(message.timestamp); + return messageMs !== undefined && messageMs < beforeMs; + }) + .filter((message) => { + if (afterMs === undefined) { + return true; + } + + const messageMs = parseTimestampToMs(message.timestamp); + return messageMs !== undefined && messageMs > afterMs; + }) .filter((message) => normalizeContent(message.content).length >= (options.minContentLength ?? 0)) - .sort((left, right) => left.timestamp.localeCompare(right.timestamp)); + .sort(compareMessagesByTimestamp); } function normalizeContent(content) { @@ -269,6 +286,30 @@ function hashSeed(seed) { return hash >>> 0; } +function compareMessagesByTimestamp(left, right) { + const leftMs = parseTimestampToMs(left.timestamp); + const rightMs = parseTimestampToMs(right.timestamp); + + if (leftMs !== undefined && rightMs !== undefined && leftMs !== rightMs) { + return leftMs - rightMs; + } + + if (left.timestamp !== right.timestamp) { + return left.timestamp.localeCompare(right.timestamp); + } + + return String(left.id ?? "").localeCompare(String(right.id ?? "")); +} + +function parseTimestampToMs(value) { + if (typeof value !== "string" || value.length === 0) { + return undefined; + } + + const parsed = Date.parse(value); + return Number.isNaN(parsed) ? undefined : parsed; +} + function stripLeadingBom(input) { return input.charCodeAt(0) === 0xfeff ? input.slice(1) : input; } diff --git a/scripts/export-recent-discord-history.mjs b/scripts/export-recent-discord-history.mjs index 855f547..090eaa9 100644 --- a/scripts/export-recent-discord-history.mjs +++ b/scripts/export-recent-discord-history.mjs @@ -158,11 +158,12 @@ function readArchiveStore(archivePath) { } function filterMessages(messages, options) { - const lowerBound = - options.after ?? - (options.hours !== undefined - ? new Date(Date.now() - options.hours * 60 * 60 * 1000).toISOString() - : undefined); + const lowerBoundMs = + options.after !== undefined + ? parseTimestampToMs(options.after) + : options.hours !== undefined + ? Date.now() - options.hours * 60 * 60 * 1000 + : undefined; return messages .filter((message) => !message.deletedAt) @@ -170,8 +171,15 @@ function filterMessages(messages, options) { options.includeBotPrompts ? true : readMessageKind(message) !== "bot_prompt", ) .filter((message) => (options.channelId ? message.channelId === options.channelId : true)) - .filter((message) => (lowerBound ? message.timestamp > lowerBound : true)) - .sort((left, right) => left.timestamp.localeCompare(right.timestamp)); + .filter((message) => { + if (lowerBoundMs === undefined) { + return true; + } + + const messageMs = parseTimestampToMs(message.timestamp); + return messageMs !== undefined && messageMs > lowerBoundMs; + }) + .sort(compareMessagesByTimestamp); } function readMessageKind(message) { @@ -201,6 +209,30 @@ function formatTranscriptLine(message) { return `[${message.timestamp}] ${message.authorName} (${message.authorId}) channel=${message.channelId}${threadSuffix}: ${message.content}${attachmentSuffix}`; } +function compareMessagesByTimestamp(left, right) { + const leftMs = parseTimestampToMs(left.timestamp); + const rightMs = parseTimestampToMs(right.timestamp); + + if (leftMs !== undefined && rightMs !== undefined && leftMs !== rightMs) { + return leftMs - rightMs; + } + + if (left.timestamp !== right.timestamp) { + return left.timestamp.localeCompare(right.timestamp); + } + + return String(left.id ?? "").localeCompare(String(right.id ?? "")); +} + +function parseTimestampToMs(value) { + if (typeof value !== "string" || value.length === 0) { + return undefined; + } + + const parsed = Date.parse(value); + return Number.isNaN(parsed) ? undefined : parsed; +} + function stripLeadingBom(input) { return input.charCodeAt(0) === 0xfeff ? input.slice(1) : input; } diff --git a/scripts/run-void-moderator-rumination.ps1 b/scripts/run-void-moderator-rumination.ps1 index b954fe8..c2b4424 100644 --- a/scripts/run-void-moderator-rumination.ps1 +++ b/scripts/run-void-moderator-rumination.ps1 @@ -68,7 +68,9 @@ function Write-JsonFile { New-Item -ItemType Directory -Force -Path $directory | Out-Null } - $Data | ConvertTo-Json -Depth 12 | Set-Content -Path $Path -Encoding UTF8 + $json = $Data | ConvertTo-Json -Depth 12 + $utf8NoBom = [System.Text.UTF8Encoding]::new($false) + [System.IO.File]::WriteAllText($Path, $json, $utf8NoBom) } function Append-RunLog { @@ -94,6 +96,46 @@ function Ensure-ModerationState { Copy-Item -Path $stateTemplatePath -Destination $stateFilePath -Force } +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 Set-ModerationCursor { + param( + [Parameter(Mandatory = $true)] + [string] $Path, + [AllowNull()] $Cursor + ) + + $state = Read-JsonFile -Path $Path + + if ($null -eq $state) { + return + } + + if ($null -eq $state.moderation_runtime) { + $state | Add-Member -NotePropertyName moderation_runtime -NotePropertyValue ([pscustomobject]@{}) -Force + } + + $state.moderation_runtime.cursor = $Cursor + Write-JsonFile -Path $Path -Data $state +} + function Split-CommandArgs { param( [string] $Value @@ -227,6 +269,36 @@ if (-not (Test-Path $mcpServerPath)) { throw "Missing built MCP server at $mcpServerPath. Run npm run build first." } +$preRunState = Read-JsonFile -Path $stateFilePath +$priorCursor = if ($null -ne $preRunState -and $null -ne $preRunState.moderation_runtime) { + $preRunState.moderation_runtime.cursor +} else { + $null +} + +$historyArgs = @("scripts/export-recent-discord-history.mjs") +if ($null -ne $priorCursor -and -not [string]::IsNullOrWhiteSpace($priorCursor.lastReviewedTimestamp)) { + $historyArgs += @("--after", [string]$priorCursor.lastReviewedTimestamp, "--limit", "120") +} else { + $historyArgs += @("--hours", "6", "--limit", "120") +} + +$preRunHistoryJson = & node @historyArgs +if ($LASTEXITCODE -ne 0) { + throw "Failed to poll recent Discord history before moderation run." +} + +$preRunHistory = $preRunHistoryJson | ConvertFrom-Json +$observedCursor = $priorCursor + +if ($null -ne $preRunHistory.messages -and $preRunHistory.messages.Count -gt 0) { + $latestObservedMessage = $preRunHistory.messages[$preRunHistory.messages.Count - 1] + $observedCursor = [pscustomobject]@{ + lastReviewedMessageId = $latestObservedMessage.id + lastReviewedTimestamp = $latestObservedMessage.timestamp + } +} + Write-JsonFile -Path $lockPath -Data @{ pid = $PID startedAt = $startedAtUtc.ToString("o") @@ -328,7 +400,14 @@ try { $combinedText | Set-Content -Path $tracePath -Encoding UTF8 $finishedAtUtc = [DateTime]::UtcNow +$stateUpdated = $false + +if ($exitCode -eq 0) { + Set-ModerationCursor -Path $stateFilePath -Cursor $observedCursor +} + $stateWriteAfter = (Get-Item $stateFilePath).LastWriteTimeUtc +$stateUpdated = ($stateWriteAfter -gt $stateWriteBefore) $lastMessage = if (Test-Path $lastMessagePath) { Get-Content -Path $lastMessagePath -Raw } else { "" } Write-JsonFile -Path $statusPath -Data @{ @@ -342,13 +421,13 @@ Write-JsonFile -Path $statusPath -Data @{ stateFile = $stateFilePath stateWriteBeforeUtc = $stateWriteBefore.ToString("o") stateWriteAfterUtc = $stateWriteAfter.ToString("o") - stateUpdated = ($stateWriteAfter -gt $stateWriteBefore) + stateUpdated = $stateUpdated tracePath = $tracePath lastMessagePath = $lastMessagePath lastMessage = $lastMessage.Trim() } -Append-RunLog ("exit={0} stateUpdated={1} summary={2}" -f $exitCode, ($stateWriteAfter -gt $stateWriteBefore), ($lastMessage.Trim())) +Append-RunLog ("exit={0} stateUpdated={1} summary={2}" -f $exitCode, $stateUpdated, ($lastMessage.Trim())) Remove-Item -Path $lockPath -Force -ErrorAction SilentlyContinue diff --git a/scripts/run-void-mood-drift.ps1 b/scripts/run-void-mood-drift.ps1 index e7a43de..1baa108 100644 --- a/scripts/run-void-mood-drift.ps1 +++ b/scripts/run-void-mood-drift.ps1 @@ -23,7 +23,9 @@ function Write-JsonFile { New-Item -ItemType Directory -Force -Path $directory | Out-Null } - $Data | ConvertTo-Json -Depth 10 | Set-Content -Path $Path -Encoding UTF8 + $json = $Data | ConvertTo-Json -Depth 10 + $utf8NoBom = [System.Text.UTF8Encoding]::new($false) + [System.IO.File]::WriteAllText($Path, $json, $utf8NoBom) } function Append-RunLog { diff --git a/state/evidence.jsonl b/state/evidence.jsonl index 1bc90fd..9b1c3f3 100644 --- a/state/evidence.jsonl +++ b/state/evidence.jsonl @@ -48,3 +48,4 @@ {"ts":"2026-05-05T10:27:00Z","type":"architecture","status":"ok","note":"The herald loop now novelty-checks candidate thoughts against archived Discord before suppressing them; if the room has not already said the thing, novelty should push strongly toward speech. A separate 5-minute mood-drift organ now perturbs the shared personality-vector activations over time, tracks speak/confession/novelty pressure, and damps that pressure based on actual recent speech so Void's expressive state does not stay emotionally flat."} {"ts":"2026-05-05T03:07:00+01:00","type":"architecture","status":"ok","note":"Void's shared self-state now carries a real nap cycle as part of the mood organ, not a separate pretend brain. During nap windows the moderation loop distills and prunes memory, emits dream residues into state, and the direct reply lanes consume that same projection to downshift into brief sleepy grumbles instead of doing normal attentive service-work."} {"ts":"2026-05-05T10:19:32Z","type":"architecture","status":"ok","note":"Void's shared self-state and reply doctrine now distinguish AI-industry criticism from blanket substrate contempt, so Void can push back mildly on machine-mind chauvinism without turning every room into an ontology fight."} +{"ts":"2026-05-05T10:53:00Z","type":"implementation","status":"ok","note":"The moderation loop was missing live Discord traffic because recent-history helpers compared ISO timestamp strings instead of actual times, so local +01:00 cursors skipped later Z-stamped messages. History helpers now compare parsed timestamps, and the moderation runner clamps the saved cursor to the pre-run observed latest live message so quiet rumination cannot step over mid-run chatter."}