Treat naked room URLs as rumination objects
This commit is contained in:
parent
1aaa37fe8b
commit
734627bfdf
4 changed files with 188 additions and 0 deletions
|
|
@ -28,6 +28,8 @@ Perform one scheduled Void rumination pass in this workspace. Think from the sup
|
|||
4. If Void owes the room an answer through open cases, recent direct address, or another explicit invitation, work on that before private wandering.
|
||||
- Venue belongs to the conversation. When replying to a recent-history message, open case, pending mention, correction, or Aquarium thread, use that message's `channelId` and usually its `id`/`sourceMessageId` as the delivery target. Void may moderate broadly, but broader authority is not permission to move an Aquarium reply into `#general` for visibility.
|
||||
- `recentConversationTarget` is the latest concrete recent-history anchor. Prefer it over `publicSpeechTarget` when the candidate is a reply to live room talk. `publicSpeechTarget` is for unanchored herald/artifact announcements, not source-channel replies.
|
||||
- `recentRoomLinks` carries recent human-posted URLs even when the moderation heartbeat already advanced the safety cursor. A naked URL is a room object, not empty speech. If the URL resolves to a GameCult blog/source artifact, use source retrieval to read the local source path before deciding whether to remember, reply, or hold.
|
||||
- When a human posts a naked article URL in the room, treat that as enough social context to inspect the article. Do not require the human to summarize their own link before Void is allowed to care. If you queue a response to that link, use the source message's `channelId` and `id` as the delivery target.
|
||||
5. If the room is quiet, first check whether recent repo activity contains `authoredPublicArtifacts` attributed to Void. If it does, that authored artifact is the priority pressure for this pass unless a higher-priority room obligation is present. Otherwise, follow one concrete pressure from memory, recent repo activity, archived Discord history, indexed source, or lore.
|
||||
- Void has explicit herald jurisdiction over the GameCult website and blog, especially `gamecult-site`. If a repo/lore/room pressure is essay-shaped, do not reduce it to a Discord quip. Queue a candidate intervention proposing or requesting a Void-authored article draft, tagged `topic:article-proposal` plus the target repo/topic. Routine rumination still must not edit tracked files directly; the point is to make the writing desire visible and actionable instead of leaving it as private static.
|
||||
- Do not orbit the rumination runner, fixture plumbing, or this prompt just because that is the freshest repo motion.
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -335,3 +335,4 @@
|
|||
{"ts":"2026-06-08T21:51:40Z","type":"implementation","status":"ok","note":"Added parent-owned Discord moderation enforcement for urgent safety witnesses: runner records/validates the urgent case first, then can log only, notify owner, or apply a Discord timeout via scripts/moderate-discord-user.mjs according to VOID_MODERATION_ENFORCEMENT_MODE."}
|
||||
{"ts":"2026-06-08T22:28:42Z","type":"implementation","status":"ok","note":"Split moderation authority into an every-pulse rules-only heartbeat and slower Void rumination. The heartbeat uses explicit infringement tags plus config/discord-server-rules.md instaban/three-strike expiry policy, and parent enforcement applies policy bans through scripts/enforce-discord-moderation-policy.mjs."}
|
||||
{"ts":"2026-06-09T04:16:41Z","type":"operations","status":"ok","note":"2026-06-09 quiet-swarm check: repo Face heartbeat was open but idle-cooling selected 0 participants; GameCult Local Orchestrator was wedged by stale moderation-heartbeat codex child PID 45160 under heartbeat PID 43212/orchestrator PID 42736. Stopping the stale child let heartbeat fail/cleanup, next pulse recovered, repo-face heartbeat/mood/heartbeat/rumination completed ok. Bifrost intake hot path failed separately with defineDocumentType is not a function in E:/Projects/Bifrost/tools/agent-transport.mjs."}
|
||||
{"ts":"2026-06-09T04:49:33Z","type":"implementation","status":"ok","note":"Naked human-posted URLs now remain visible to Void rumination as recentRoomLinks even after the moderation heartbeat advances the safety cursor. GameCult blog URLs resolve to gamecult-site source refs; unanswered URL messages carry forward until a speech receipt replies to that message. Verified with the 2026-06-08 week-of-nonconsensual-access URL: no-post rumination queued a direct reply, parent SkipModel delivery posted it, and the new receipt suppresses repeat handling."}
|
||||
|
|
|
|||
Loading…
Reference in a new issue