Fix moderation history time handling

This commit is contained in:
Meta Vens 2026-05-05 11:54:01 +01:00
commit f9ff9d2cc9
5 changed files with 169 additions and 14 deletions

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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

View file

@ -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 {

View file

@ -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."}