Treat naked room URLs as rumination objects

This commit is contained in:
Meta Vens 2026-06-09 05:50:01 +01:00
commit 734627bfdf
4 changed files with 188 additions and 0 deletions

View file

@ -197,6 +197,154 @@ function Project-RecentConversationTargetForRumination {
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)

View file

@ -1365,6 +1365,37 @@ if ($messageCount -gt 0) {
}
}
$recentRoomLinkHistory = $null
$recentRoomLinks = @{
source = "recent_discord_url_scan"
itemCount = 0
items = @()
}
if (-not $ModerationHeartbeatOnly) {
$recentRoomLinkArgs = @($recentHistoryScriptPath, "--hours", "24", "--limit", "200")
if (-not [string]::IsNullOrWhiteSpace($publicRoomChannelId)) {
$recentRoomLinkArgs += @("--channel-id", $publicRoomChannelId)
}
try {
$recentRoomLinkHistory = Invoke-NodeJson -Arguments $recentRoomLinkArgs
$receiptReplyMessageIds = @(
@(Convert-ToValueArray -Value $typedState.speechReceipts.recentReceipts) |
ForEach-Object { Get-ObjectPropertyString -Value $_ -Name "replyToMessageId" } |
Where-Object { -not [string]::IsNullOrWhiteSpace($_) }
)
$recentRoomLinks = Project-RecentRoomLinksForRumination -History $recentRoomLinkHistory -Now $startedAtUtc -MinTimestampInclusive $priorCursorTimestamp -ReceiptReplyMessageIds $receiptReplyMessageIds
} catch {
$recentRoomLinks = @{
source = "recent_discord_url_scan"
status = "failed"
error = $_.Exception.Message
itemCount = 0
items = @()
}
}
}
$recentRoomLinkCount = [int](Get-ObjectPropertyValue -Value $recentRoomLinks -Name "itemCount")
$repoActivity = $null
if ($ModerationHeartbeatOnly) {
$repoActivity = @{
@ -1430,6 +1461,7 @@ if ($ModerationHeartbeatOnly) {
urgentModerationWitnesses = $urgentModerationWitnesses
pendingMentions = $pendingMentions
recentConversationTarget = Project-RecentConversationTargetForRumination -History $history -PersonaName $publicRoomPersonaName -PersonaAvatarUrl $publicRoomPersonaAvatarUrl
recentRoomLinks = $recentRoomLinks
publicSpeechTarget = Project-PublicSpeechTargetForRumination -ChannelId $publicRoomChannelId -PersonaName $publicRoomPersonaName -PersonaAvatarUrl $publicRoomPersonaAvatarUrl
deliverableCandidateCount = [int]$deliverableCandidates.Count
candidateInterventions = @(Project-InterventionsForRumination -Interventions $typedState.candidateInterventions.interventions -Now $startedAtUtc)
@ -1447,6 +1479,9 @@ $openCaseCount = @(
Where-Object { Test-OpenCaseRequiresRumination -Case $_ }
).Count
if (-not $ModerationHeartbeatOnly -and $isNapping -and $messageCount -eq 0 -and $openCaseCount -eq 0 -and $deliverableCandidates.Count -eq 0) {
if ($recentRoomLinkCount -gt 0) {
Append-RunLog ("napping: recent room links present ({0}); keeping awake rumination path available." -f $recentRoomLinkCount)
} else {
Append-RunLog "napping: no new room messages or open cases; skipping awake rumination."
Write-JsonFile -Path $operationOutputPath -Data @()
$finishedAtUtc = [DateTime]::UtcNow
@ -1469,6 +1504,7 @@ if (-not $ModerationHeartbeatOnly -and $isNapping -and $messageCount -eq 0 -and
}
Remove-Item -Path $lockPath -Force -ErrorAction SilentlyContinue
return
}
}
$stateWriteBefore = if (Test-Path $stateFilePath) { (Get-Item $stateFilePath).LastWriteTimeUtc } else { [DateTime]::MinValue }
@ -1650,6 +1686,7 @@ $finalStatus = [ordered]@{
contextPath = [string]$contextPath
operationOutputPath = [string]$operationOutputPath
observedMessageCount = [int]$messageCount
recentRoomLinkCount = [int]$recentRoomLinkCount
previousCursorTimestamp = [string](Get-ObjectPropertyString -Value $priorCursor -Name "lastReviewedTimestamp")
observedCursorTimestamp = [string]$observedCursorTimestamp
proposedOperationCount = [int]@($proposedOperations).Count