Require Bifrost for moderation delivery

This commit is contained in:
Meta Vens 2026-06-04 23:37:12 +01:00
commit ed77f79306
8 changed files with 80 additions and 126 deletions

View file

@ -43,11 +43,6 @@ $contextProjectionScriptPath = Join-Path $repoRoot "scripts\lib\void-rumination-
$recentHistoryScriptPath = Join-Path $repoRoot "scripts\export-recent-discord-history.mjs"
$repoActivityScriptPath = Join-Path $repoRoot "scripts\export-recent-repo-activity.mjs"
$selfStateScriptPath = Join-Path $repoRoot "scripts\void-self-state.mjs"
$sendMessageScriptPath = if (-not [string]::IsNullOrWhiteSpace($env:VOID_SEND_DISCORD_SCRIPT)) {
$ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($env:VOID_SEND_DISCORD_SCRIPT)
} else {
Join-Path $repoRoot "scripts\send-discord-message.mjs"
}
$bifrostRoot = if (-not [string]::IsNullOrWhiteSpace($env:BIFROST_ROOT)) {
$ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($env:BIFROST_ROOT)
} else {
@ -58,13 +53,7 @@ $bifrostBridgeScriptPath = if (-not [string]::IsNullOrWhiteSpace($env:VOID_BIFRO
} else {
Join-Path $bifrostRoot "tools\bifrost-bridge.mjs"
}
$discordTransportMode = if (-not [string]::IsNullOrWhiteSpace($env:VOID_DISCORD_TRANSPORT)) {
$env:VOID_DISCORD_TRANSPORT.Trim().ToLowerInvariant()
} elseif (Test-Path $bifrostBridgeScriptPath) {
"bifrost"
} else {
"direct"
}
$discordTransportMode = "bifrost"
$startedAtUtc = [DateTime]::UtcNow
. $contextProjectionScriptPath
@ -839,42 +828,25 @@ function Invoke-CandidateInterventionDeliveryFromIntervention {
if ([string]::IsNullOrWhiteSpace($interventionId) -or [string]::IsNullOrWhiteSpace($draft)) {
return $null
}
Assert-BifrostBridgeAvailable
$contentPath = Join-Path $statusDir ("moderation-rumination-speech-{0}.txt" -f ([Guid]::NewGuid().ToString("n")))
$utf8NoBom = [System.Text.UTF8Encoding]::new($false)
[System.IO.File]::WriteAllText($contentPath, $draft, $utf8NoBom)
$mode = Get-ObjectPropertyString -Value $deliveryTarget -Name "mode"
$arguments = @()
if ($discordTransportMode -eq "bifrost") {
if ($mode -eq "owner_dm") {
throw "Candidate intervention '$interventionId' targets owner_dm. Bifrost is the default public transport; use VOID_DISCORD_TRANSPORT=direct only for standalone owner-private installs."
}
$channelId = Get-ObjectPropertyString -Value $deliveryTarget -Name "channelId"
if ([string]::IsNullOrWhiteSpace($channelId)) {
throw "Candidate intervention '$interventionId' has deliveryTarget without channelId."
}
$arguments = @($bifrostBridgeScriptPath, "discord-post", "--channel-id", $channelId, "--content-file", $contentPath)
} else {
$arguments = @($sendMessageScriptPath, "--content-file", $contentPath)
if ($mode -eq "owner_dm") {
$arguments += "--owner-dm"
} else {
$channelId = Get-ObjectPropertyString -Value $deliveryTarget -Name "channelId"
if ([string]::IsNullOrWhiteSpace($channelId)) {
throw "Candidate intervention '$interventionId' has deliveryTarget without channelId."
}
$arguments += @("--channel-id", $channelId)
}
if ($mode -eq "owner_dm") {
throw "Candidate intervention '$interventionId' targets owner_dm, but VoidBot moderation delivery now requires Bifrost Discord transport."
}
$channelId = Get-ObjectPropertyString -Value $deliveryTarget -Name "channelId"
if ([string]::IsNullOrWhiteSpace($channelId)) {
throw "Candidate intervention '$interventionId' has deliveryTarget without channelId."
}
$arguments = @($bifrostBridgeScriptPath, "discord-post", "--channel-id", $channelId, "--content-file", $contentPath)
$replyToMessageId = Get-ObjectPropertyString -Value $deliveryTarget -Name "replyToMessageId"
if (-not [string]::IsNullOrWhiteSpace($replyToMessageId)) {
if ($discordTransportMode -eq "bifrost") {
$arguments += @("--reply-to-message-id", $replyToMessageId)
} else {
$arguments += @("--reply-to", $replyToMessageId)
}
$arguments += @("--reply-to-message-id", $replyToMessageId)
}
$personaName = Get-ObjectPropertyString -Value $deliveryTarget -Name "personaName"
@ -904,17 +876,8 @@ function Invoke-CandidateInterventionDeliveryFromIntervention {
Remove-Item -LiteralPath $contentPath -Force -ErrorAction SilentlyContinue
}
if ($discordTransportMode -eq "bifrost") {
$bridgeReceiptText = [string]($sendOutput -join [Environment]::NewLine)
$speech = Convert-BifrostBridgeReceiptToSpeechStatus -Receipt ($bridgeReceiptText | ConvertFrom-Json) -Preview $draft
return Convert-LastSpeechToSpokenCandidateOperation -InterventionId $interventionId -Speech $speech
}
$lastSpeechPath = Join-Path $statusDir "void-last-speech.json"
if (-not (Test-Path $lastSpeechPath)) {
throw "Candidate intervention delivery did not write last speech status."
}
$speech = Read-JsonFile -Path $lastSpeechPath
$bridgeReceiptText = [string]($sendOutput -join [Environment]::NewLine)
$speech = Convert-BifrostBridgeReceiptToSpeechStatus -Receipt ($bridgeReceiptText | ConvertFrom-Json) -Preview $draft
return Convert-LastSpeechToSpokenCandidateOperation -InterventionId $interventionId -Speech $speech
}
@ -928,17 +891,35 @@ function Convert-BifrostBridgeReceiptToSpeechStatus {
return $null
}
$action = Get-ObjectPropertyString -Value $Receipt -Name "action"
$mode = switch ($action) {
"discord-post" { "channel" }
"discord-dm" { "user_dm" }
default { $action }
}
return [pscustomobject]@{
sentAt = [DateTime]::UtcNow.ToString("o")
mode = Get-ObjectPropertyString -Value $Receipt -Name "action"
mode = $mode
transport = Get-ObjectPropertyString -Value $Receipt -Name "transport"
channelId = Get-ObjectPropertyString -Value $Receipt -Name "channelId"
messageId = Get-ObjectPropertyString -Value $Receipt -Name "messageId"
recipientId = Get-ObjectPropertyString -Value $Receipt -Name "recipientId"
replyToMessageId = Get-ObjectPropertyString -Value $Receipt -Name "replyToMessageId"
personaName = Get-ObjectPropertyString -Value $Receipt -Name "personaName"
personaAvatarUrl = Get-ObjectPropertyString -Value $Receipt -Name "personaAvatarUrl"
contentLength = if ($null -ne $Preview) { [int]$Preview.Length } else { $null }
chunkCount = 1
preview = if ([string]::IsNullOrWhiteSpace($Preview)) { $null } else { $Preview.Substring(0, [Math]::Min(280, $Preview.Length)) }
}
}
function Assert-BifrostBridgeAvailable {
if (-not (Test-Path $bifrostBridgeScriptPath)) {
throw "Bifrost bridge helper is required for VoidBot moderation delivery but is missing at $bifrostBridgeScriptPath"
}
}
function Get-PendingModerationStatusNotices {
param($TypedState)
@ -1012,16 +993,13 @@ function Invoke-ModerationStatusNoticeDelivery {
) {
return $null
}
Assert-BifrostBridgeAvailable
$contentPath = Join-Path $statusDir ("moderation-status-dm-{0}.txt" -f ([Guid]::NewGuid().ToString("n")))
$utf8NoBom = [System.Text.UTF8Encoding]::new($false)
[System.IO.File]::WriteAllText($contentPath, $body, $utf8NoBom)
$arguments = if ($discordTransportMode -eq "bifrost") {
@($bifrostBridgeScriptPath, "discord-dm", "--recipient-id", $userId, "--content-file", $contentPath)
} else {
@($sendMessageScriptPath, "--content-file", $contentPath, "--user-dm", $userId)
}
$arguments = @($bifrostBridgeScriptPath, "discord-dm", "--recipient-id", $userId, "--content-file", $contentPath)
$previousVoidStatusDir = $env:VOID_STATUS_DIR
try {
$env:VOID_STATUS_DIR = $statusDir
@ -1039,17 +1017,8 @@ function Invoke-ModerationStatusNoticeDelivery {
Remove-Item -LiteralPath $contentPath -Force -ErrorAction SilentlyContinue
}
if ($discordTransportMode -eq "bifrost") {
$bridgeReceiptText = [string]($sendOutput -join [Environment]::NewLine)
$speech = Convert-BifrostBridgeReceiptToSpeechStatus -Receipt ($bridgeReceiptText | ConvertFrom-Json) -Preview $body
return Convert-LastSpeechToModerationStatusNoticeSentOperation -UserId $userId -NoticeId $noticeId -Speech $speech
}
$lastSpeechPath = Join-Path $statusDir "void-last-speech.json"
if (-not (Test-Path $lastSpeechPath)) {
throw "Moderation status DM '$noticeId' did not write last speech status."
}
$speech = Read-JsonFile -Path $lastSpeechPath
$bridgeReceiptText = [string]($sendOutput -join [Environment]::NewLine)
$speech = Convert-BifrostBridgeReceiptToSpeechStatus -Receipt ($bridgeReceiptText | ConvertFrom-Json) -Preview $body
return Convert-LastSpeechToModerationStatusNoticeSentOperation -UserId $userId -NoticeId $noticeId -Speech $speech
}
@ -1212,16 +1181,8 @@ foreach ($requiredPath in @($contextProjectionScriptPath, $recentHistoryScriptPa
throw "Missing required helper at $requiredPath"
}
}
if ($discordTransportMode -eq "bifrost") {
if (-not (Test-Path $bifrostBridgeScriptPath)) {
throw "VOID_DISCORD_TRANSPORT=bifrost but Bifrost bridge helper is missing at $bifrostBridgeScriptPath"
}
} elseif ($discordTransportMode -eq "direct") {
if (-not (Test-Path $sendMessageScriptPath)) {
throw "VOID_DISCORD_TRANSPORT=direct but direct Discord helper is missing at $sendMessageScriptPath"
}
} else {
throw "Unsupported VOID_DISCORD_TRANSPORT '$discordTransportMode'. Use bifrost or direct."
if (-not [string]::IsNullOrWhiteSpace($env:VOID_DISCORD_TRANSPORT) -and $env:VOID_DISCORD_TRANSPORT.Trim().ToLowerInvariant() -ne "bifrost") {
throw "VOID_DISCORD_TRANSPORT=$($env:VOID_DISCORD_TRANSPORT) is no longer supported. VoidBot moderation delivery requires Bifrost."
}
if (-not (Test-Path $promptTemplatePath)) {
throw "Missing rumination prompt template at $promptTemplatePath"

View file

@ -8,7 +8,7 @@ Set-Location -LiteralPath $repoRoot
$tempRoot = Join-Path $env:TEMP ("void-moderation-status-fixture-" + [guid]::NewGuid().ToString("n"))
$stateFilePath = Join-Path $tempRoot "void-self-state.cc"
$operationPath = Join-Path $tempRoot "operation.json"
$fakeSendPath = Join-Path $tempRoot "fake-send-discord-message.mjs"
$fakeBridgePath = Join-Path $tempRoot "fake-bifrost-bridge.mjs"
$fixtureStatusDir = Join-Path $tempRoot "status"
$fixtureLogDir = Join-Path $tempRoot "logs"
$statusPath = Join-Path $fixtureStatusDir "moderation-rumination.json"
@ -17,38 +17,34 @@ $checkPath = Join-Path $tempRoot "check-moderation-status.mjs"
New-Item -ItemType Directory -Force -Path $tempRoot | Out-Null
try {
$fakeSendSource = @'
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { dirname, resolve } from "node:path";
$fakeBridgeSource = @'
import { readFileSync } from "node:fs";
import { resolve } from "node:path";
const statusDir = resolve(process.env.VOID_STATUS_DIR);
const command = process.argv[2];
const contentFileIndex = process.argv.indexOf("--content-file");
const userDmIndex = process.argv.indexOf("--user-dm");
const recipientIndex = process.argv.indexOf("--recipient-id");
if (contentFileIndex < 0 || userDmIndex < 0) {
console.error("fake sender expected --content-file and --user-dm");
if (command !== "discord-dm" || contentFileIndex < 0 || recipientIndex < 0) {
console.error("fake Bifrost bridge expected discord-dm, --content-file, and --recipient-id");
process.exit(2);
}
const content = readFileSync(resolve(process.argv[contentFileIndex + 1]), "utf8");
const recipientId = process.argv[userDmIndex + 1];
const recipientId = process.argv[recipientIndex + 1];
const payload = {
sentAt: "2026-06-04T12:00:00.000Z",
mode: "user_dm",
action: "discord-dm",
ok: true,
transport: "bot",
channelId: "fixture-dm-channel",
messageId: "fixture-dm-message",
recipientId,
contentLength: content.trim().length,
chunkCount: 1,
preview: content.trim().slice(0, 280),
};
mkdirSync(statusDir, { recursive: true });
writeFileSync(resolve(statusDir, "void-last-speech.json"), `${JSON.stringify(payload, null, 2)}\n`, "utf8");
writeFileSync(resolve(statusDir, "void-speech-log.jsonl"), `${JSON.stringify(payload)}\n`, { encoding: "utf8", flag: "a" });
process.stdout.write(JSON.stringify({ ok: true, mode: "user_dm", channelId: payload.channelId, recipientId }) + "\n");
process.stdout.write(JSON.stringify(payload) + "\n");
'@
[System.IO.File]::WriteAllText($fakeSendPath, $fakeSendSource, [System.Text.UTF8Encoding]::new($false))
[System.IO.File]::WriteAllText($fakeBridgePath, $fakeBridgeSource, [System.Text.UTF8Encoding]::new($false))
$expiredStrikeOperation = @{
operation = "upsert_moderation_user_status"
@ -88,22 +84,22 @@ process.stdout.write(JSON.stringify({ ok: true, mode: "user_dm", channelId: payl
$previousStatusDir = $env:VOID_RUMINATION_STATUS_DIR
$previousLogDir = $env:VOID_RUMINATION_LOG_DIR
$previousSendScript = $env:VOID_SEND_DISCORD_SCRIPT
$previousBridgeScript = $env:VOID_BIFROST_BRIDGE_SCRIPT
$previousDiscordTransport = $env:VOID_DISCORD_TRANSPORT
$previousDisableRepoCursorAdvance = $env:VOID_RUMINATION_DISABLE_REPO_CURSOR_ADVANCE
try {
$env:VOID_RUMINATION_STATUS_DIR = $fixtureStatusDir
$env:VOID_RUMINATION_LOG_DIR = $fixtureLogDir
$env:VOID_SEND_DISCORD_SCRIPT = $fakeSendPath
$env:VOID_DISCORD_TRANSPORT = "direct"
$env:VOID_BIFROST_BRIDGE_SCRIPT = $fakeBridgePath
$env:VOID_DISCORD_TRANSPORT = "bifrost"
$env:VOID_RUMINATION_DISABLE_REPO_CURSOR_ADVANCE = "1"
powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\run-void-moderator-rumination.ps1 -StateFilePath $stateFilePath -SkipModel | Out-Null
} finally {
$env:VOID_RUMINATION_STATUS_DIR = $previousStatusDir
$env:VOID_RUMINATION_LOG_DIR = $previousLogDir
$env:VOID_SEND_DISCORD_SCRIPT = $previousSendScript
$env:VOID_BIFROST_BRIDGE_SCRIPT = $previousBridgeScript
$env:VOID_DISCORD_TRANSPORT = $previousDiscordTransport
$env:VOID_RUMINATION_DISABLE_REPO_CURSOR_ADVANCE = $previousDisableRepoCursorAdvance
}

View file

@ -8,7 +8,7 @@ Set-Location -LiteralPath $repoRoot
$tempRoot = Join-Path $env:TEMP ("void-rumination-speech-fixture-" + [guid]::NewGuid().ToString("n"))
$stateFilePath = Join-Path $tempRoot "void-self-state.cc"
$fakeCodexPath = Join-Path $tempRoot "fake-codex-rumination-speech.mjs"
$fakeSendPath = Join-Path $tempRoot "fake-send-discord-message.mjs"
$fakeBridgePath = Join-Path $tempRoot "fake-bifrost-bridge.mjs"
$cursorOperationPath = Join-Path $tempRoot "cursor.json"
$fixtureStatusDir = Join-Path $tempRoot "status"
$fixtureLogDir = Join-Path $tempRoot "logs"
@ -131,19 +131,19 @@ process.stdin.on("end", () => {
'@
[System.IO.File]::WriteAllText($fakeCodexPath, $fakeCodexSource, [System.Text.UTF8Encoding]::new($false))
$fakeSendSource = @'
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { dirname, resolve } from "node:path";
$fakeBridgeSource = @'
import { readFileSync } from "node:fs";
import { resolve } from "node:path";
const statusDir = resolve(process.env.VOID_STATUS_DIR);
const command = process.argv[2];
const contentFileIndex = process.argv.indexOf("--content-file");
const channelIndex = process.argv.indexOf("--channel-id");
const replyIndex = process.argv.indexOf("--reply-to");
const replyIndex = process.argv.indexOf("--reply-to-message-id");
const personaIndex = process.argv.indexOf("--persona-name");
const personaAvatarIndex = process.argv.indexOf("--persona-avatar-url");
if (contentFileIndex < 0 || channelIndex < 0) {
console.error("fake sender expected --content-file and --channel-id");
if (command !== "discord-post" || contentFileIndex < 0 || channelIndex < 0) {
console.error("fake Bifrost bridge expected discord-post, --content-file, and --channel-id");
process.exit(2);
}
@ -153,24 +153,20 @@ const replyToMessageId = replyIndex >= 0 ? process.argv[replyIndex + 1] : undefi
const personaName = personaIndex >= 0 ? process.argv[personaIndex + 1] : undefined;
const personaAvatarUrl = personaAvatarIndex >= 0 ? process.argv[personaAvatarIndex + 1] : undefined;
const payload = {
sentAt: "2026-05-17T03:01:00.000Z",
mode: "channel",
transport: personaName ? "webhook" : "bot",
action: "discord-post",
ok: true,
channelId,
messageId: "fixture-bifrost-speech-message",
transport: personaName ? "webhook" : "bot",
replyToMessageId,
personaName,
personaAvatarUrl,
contentLength: content.trim().length,
chunkCount: 1,
preview: content.trim().slice(0, 280),
};
mkdirSync(statusDir, { recursive: true });
writeFileSync(resolve(statusDir, "void-last-speech.json"), `${JSON.stringify(payload, null, 2)}\n`, "utf8");
writeFileSync(resolve(statusDir, "void-speech-log.jsonl"), `${JSON.stringify(payload)}\n`, { encoding: "utf8", flag: "a" });
process.stdout.write(JSON.stringify({ ok: true, mode: "channel", channelId }) + "\n");
process.stdout.write(JSON.stringify(payload) + "\n");
'@
[System.IO.File]::WriteAllText($fakeSendPath, $fakeSendSource, [System.Text.UTF8Encoding]::new($false))
[System.IO.File]::WriteAllText($fakeBridgePath, $fakeBridgeSource, [System.Text.UTF8Encoding]::new($false))
$cursorOperation = @{
operation = "record_reviewed_messages"
@ -220,7 +216,7 @@ process.stdout.write(JSON.stringify({ ok: true, mode: "channel", channelId }) +
$previousFixtureOutput = $env:VOID_RUMINATION_FIXTURE_OPERATION_OUTPUT
$previousStatusDir = $env:VOID_RUMINATION_STATUS_DIR
$previousLogDir = $env:VOID_RUMINATION_LOG_DIR
$previousSendScript = $env:VOID_SEND_DISCORD_SCRIPT
$previousBridgeScript = $env:VOID_BIFROST_BRIDGE_SCRIPT
$previousDiscordTransport = $env:VOID_DISCORD_TRANSPORT
$previousDisableRepoCursorAdvance = $env:VOID_RUMINATION_DISABLE_REPO_CURSOR_ADVANCE
$previousVoidAvatarUrl = $env:DISCORD_PERSONA_AVATAR_URL_VOID
@ -231,8 +227,8 @@ process.stdout.write(JSON.stringify({ ok: true, mode: "channel", channelId }) +
$env:VOID_RUMINATION_FIXTURE_OPERATION_OUTPUT = $statusOperationPath
$env:VOID_RUMINATION_STATUS_DIR = $fixtureStatusDir
$env:VOID_RUMINATION_LOG_DIR = $fixtureLogDir
$env:VOID_SEND_DISCORD_SCRIPT = $fakeSendPath
$env:VOID_DISCORD_TRANSPORT = "direct"
$env:VOID_BIFROST_BRIDGE_SCRIPT = $fakeBridgePath
$env:VOID_DISCORD_TRANSPORT = "bifrost"
$env:VOID_RUMINATION_DISABLE_REPO_CURSOR_ADVANCE = "1"
$env:DISCORD_PERSONA_AVATAR_URL_VOID = "https://example.invalid/void-fixture-avatar.png"
@ -243,7 +239,7 @@ process.stdout.write(JSON.stringify({ ok: true, mode: "channel", channelId }) +
$env:VOID_RUMINATION_FIXTURE_OPERATION_OUTPUT = $previousFixtureOutput
$env:VOID_RUMINATION_STATUS_DIR = $previousStatusDir
$env:VOID_RUMINATION_LOG_DIR = $previousLogDir
$env:VOID_SEND_DISCORD_SCRIPT = $previousSendScript
$env:VOID_BIFROST_BRIDGE_SCRIPT = $previousBridgeScript
$env:VOID_DISCORD_TRANSPORT = $previousDiscordTransport
$env:VOID_RUMINATION_DISABLE_REPO_CURSOR_ADVANCE = $previousDisableRepoCursorAdvance
$env:DISCORD_PERSONA_AVATAR_URL_VOID = $previousVoidAvatarUrl