Move rumination speech under parent control
This commit is contained in:
parent
9944076e7b
commit
d154aabc69
13 changed files with 374 additions and 22 deletions
|
|
@ -38,6 +38,11 @@ $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"
|
||||
}
|
||||
$startedAtUtc = [DateTime]::UtcNow
|
||||
|
||||
. $contextProjectionScriptPath
|
||||
|
|
@ -358,6 +363,95 @@ function Convert-LastSpeechToReceiptOperation {
|
|||
}
|
||||
}
|
||||
|
||||
function Convert-LastSpeechToSpokenCandidateOperation {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string] $InterventionId,
|
||||
$Speech
|
||||
)
|
||||
|
||||
$receiptOperation = Convert-LastSpeechToReceiptOperation -Speech $Speech
|
||||
if ($null -eq $receiptOperation) {
|
||||
return $null
|
||||
}
|
||||
|
||||
return @{
|
||||
operation = "mark_candidate_intervention_spoken"
|
||||
interventionId = $InterventionId
|
||||
receipt = $receiptOperation.receipt
|
||||
}
|
||||
}
|
||||
|
||||
function Invoke-CandidateInterventionDelivery {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
$Operation
|
||||
)
|
||||
|
||||
$intervention = Get-ObjectPropertyValue -Value $Operation -Name "intervention"
|
||||
$deliveryTarget = Get-ObjectPropertyValue -Value $intervention -Name "deliveryTarget"
|
||||
if ($null -eq $deliveryTarget) {
|
||||
return $null
|
||||
}
|
||||
|
||||
$interventionId = Get-ObjectPropertyString -Value $intervention -Name "interventionId"
|
||||
$draft = Get-ObjectPropertyString -Value $intervention -Name "draft"
|
||||
if ([string]::IsNullOrWhiteSpace($interventionId) -or [string]::IsNullOrWhiteSpace($draft)) {
|
||||
return $null
|
||||
}
|
||||
|
||||
$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)
|
||||
|
||||
$arguments = @($sendMessageScriptPath, "--content-file", $contentPath)
|
||||
$mode = Get-ObjectPropertyString -Value $deliveryTarget -Name "mode"
|
||||
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)
|
||||
}
|
||||
|
||||
$replyToMessageId = Get-ObjectPropertyString -Value $deliveryTarget -Name "replyToMessageId"
|
||||
if (-not [string]::IsNullOrWhiteSpace($replyToMessageId)) {
|
||||
$arguments += @("--reply-to", $replyToMessageId)
|
||||
}
|
||||
|
||||
$personaName = Get-ObjectPropertyString -Value $deliveryTarget -Name "personaName"
|
||||
if (-not [string]::IsNullOrWhiteSpace($personaName)) {
|
||||
$arguments += @("--persona-name", $personaName)
|
||||
}
|
||||
|
||||
$personaAvatarUrl = Get-ObjectPropertyString -Value $deliveryTarget -Name "personaAvatarUrl"
|
||||
if (-not [string]::IsNullOrWhiteSpace($personaAvatarUrl)) {
|
||||
$arguments += @("--persona-avatar-url", $personaAvatarUrl)
|
||||
}
|
||||
|
||||
$previousVoidStatusDir = $env:VOID_STATUS_DIR
|
||||
try {
|
||||
$env:VOID_STATUS_DIR = $statusDir
|
||||
& node @arguments | Out-Null
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "Candidate intervention delivery failed with exit code $LASTEXITCODE."
|
||||
}
|
||||
} finally {
|
||||
$env:VOID_STATUS_DIR = $previousVoidStatusDir
|
||||
Remove-Item -LiteralPath $contentPath -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
|
||||
$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
|
||||
return Convert-LastSpeechToSpokenCandidateOperation -InterventionId $interventionId -Speech $speech
|
||||
}
|
||||
|
||||
trap {
|
||||
$finishedAtUtc = [DateTime]::UtcNow
|
||||
$failureMessage = $_.Exception.Message
|
||||
|
|
@ -405,7 +499,7 @@ if (Test-Path $operationOutputPath) {
|
|||
Remove-Item -LiteralPath $operationOutputPath -Force
|
||||
}
|
||||
|
||||
foreach ($requiredPath in @($contextProjectionScriptPath, $recentHistoryScriptPath, $repoActivityScriptPath, $selfStateScriptPath)) {
|
||||
foreach ($requiredPath in @($contextProjectionScriptPath, $recentHistoryScriptPath, $repoActivityScriptPath, $selfStateScriptPath, $sendMessageScriptPath)) {
|
||||
if (-not (Test-Path $requiredPath)) {
|
||||
throw "Missing required helper at $requiredPath"
|
||||
}
|
||||
|
|
@ -596,12 +690,21 @@ if (
|
|||
}
|
||||
}
|
||||
|
||||
$lastSpeechPath = Join-Path $statusDir "void-last-speech.json"
|
||||
if ((-not $NoPost) -and (Test-Path $lastSpeechPath)) {
|
||||
$speech = Read-JsonFile -Path $lastSpeechPath
|
||||
$receiptOperation = Convert-LastSpeechToReceiptOperation -Speech $speech
|
||||
if ($null -ne $receiptOperation) {
|
||||
$appliedOperations += Apply-TypedOperation -Operation $receiptOperation
|
||||
$deliveredCandidateCount = 0
|
||||
if (-not $NoPost) {
|
||||
foreach ($operation in $proposedOperations) {
|
||||
if ($deliveredCandidateCount -ge 1) {
|
||||
break
|
||||
}
|
||||
$operationName = Get-ObjectPropertyString -Value $operation -Name "operation"
|
||||
if ($operationName -ne "queue_candidate_intervention") {
|
||||
continue
|
||||
}
|
||||
$spokenOperation = Invoke-CandidateInterventionDelivery -Operation $operation
|
||||
if ($null -ne $spokenOperation) {
|
||||
$appliedOperations += Apply-TypedOperation -Operation $spokenOperation
|
||||
$deliveredCandidateCount += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -628,6 +731,7 @@ $finalStatus = [ordered]@{
|
|||
observedCursorTimestamp = [string]$observedCursorTimestamp
|
||||
proposedOperationCount = [int]@($proposedOperations).Count
|
||||
appliedOperationCount = [int]@($appliedOperations).Count
|
||||
deliveredCandidateCount = [int]$deliveredCandidateCount
|
||||
stateUpdated = [bool](@($appliedOperations).Count -gt 0)
|
||||
tracePath = [string]$tracePath
|
||||
lastMessagePath = [string]$lastMessagePath
|
||||
|
|
|
|||
|
|
@ -560,10 +560,13 @@ function findSplitIndex(input, limit) {
|
|||
}
|
||||
|
||||
function writeLastSpeechStatus(payload) {
|
||||
const statusPath = resolve(repoRoot, ".voidbot/status/void-last-speech.json");
|
||||
const statusDir = process.env.VOID_STATUS_DIR
|
||||
? resolve(process.env.VOID_STATUS_DIR)
|
||||
: resolve(repoRoot, ".voidbot/status");
|
||||
const statusPath = resolve(statusDir, "void-last-speech.json");
|
||||
mkdirSync(dirname(statusPath), { recursive: true });
|
||||
writeFileSync(statusPath, `${JSON.stringify(payload, null, 2)}\n`, "utf8");
|
||||
const logPath = resolve(repoRoot, ".voidbot/status/void-speech-log.jsonl");
|
||||
const logPath = resolve(statusDir, "void-speech-log.jsonl");
|
||||
writeFileSync(logPath, `${JSON.stringify(payload)}\n`, { encoding: "utf8", flag: "a" });
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -36,9 +36,13 @@ process.stdin.on("end", () => {
|
|||
console.error("fixture prompt did not contain the short-term rumination contract");
|
||||
process.exit(2);
|
||||
}
|
||||
if (!prompt.includes("Do not orbit the rumination runner")) {
|
||||
console.error("fixture prompt did not contain the self-orbit guard");
|
||||
process.exit(3);
|
||||
}
|
||||
if (prompt.includes("moderation-agent-state.json") && !prompt.includes("Do not read or write")) {
|
||||
console.error("fixture prompt referenced legacy state outside the boundary warning");
|
||||
process.exit(3);
|
||||
process.exit(4);
|
||||
}
|
||||
|
||||
const operations = [
|
||||
|
|
|
|||
181
scripts/smoke-void-rumination-speech-fixture.ps1
Normal file
181
scripts/smoke-void-rumination-speech-fixture.ps1
Normal file
|
|
@ -0,0 +1,181 @@
|
|||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = "Stop"
|
||||
$ProgressPreference = "SilentlyContinue"
|
||||
|
||||
$repoRoot = Split-Path -Parent $PSScriptRoot
|
||||
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"
|
||||
$cursorOperationPath = Join-Path $tempRoot "cursor.json"
|
||||
$fixtureStatusDir = Join-Path $tempRoot "status"
|
||||
$fixtureLogDir = Join-Path $tempRoot "logs"
|
||||
$statusOperationPath = Join-Path $fixtureStatusDir "moderation-rumination-operations.json"
|
||||
$statusPath = Join-Path $fixtureStatusDir "moderation-rumination.json"
|
||||
|
||||
New-Item -ItemType Directory -Force -Path $tempRoot | Out-Null
|
||||
|
||||
try {
|
||||
$fakeCodexSource = @'
|
||||
import { mkdirSync, writeFileSync } from "node:fs";
|
||||
import { dirname, resolve } from "node:path";
|
||||
|
||||
const operationOutputPath = resolve(process.env.VOID_RUMINATION_FIXTURE_OPERATION_OUTPUT);
|
||||
const lastMessageIndex = process.argv.indexOf("-o");
|
||||
const lastMessagePath = lastMessageIndex >= 0 ? process.argv[lastMessageIndex + 1] : undefined;
|
||||
|
||||
let prompt = "";
|
||||
process.stdin.setEncoding("utf8");
|
||||
process.stdin.on("data", (chunk) => {
|
||||
prompt += chunk;
|
||||
});
|
||||
process.stdin.on("end", () => {
|
||||
if (!prompt.includes("The parent runner owns posting and receipts")) {
|
||||
console.error("fixture prompt did not contain the parent-owned speech contract");
|
||||
process.exit(2);
|
||||
}
|
||||
|
||||
const operations = [
|
||||
{
|
||||
operation: "queue_candidate_intervention",
|
||||
intervention: {
|
||||
interventionId: "fixture-parent-owned-speech",
|
||||
kind: "ripe_thought_share",
|
||||
status: "queued",
|
||||
target: {
|
||||
kind: "room",
|
||||
id: "fixture-channel",
|
||||
label: "Fixture channel",
|
||||
},
|
||||
summary: "Parent-owned speech should close the action loop.",
|
||||
draft: "This is a fixture sentence proving the parent owns the mouth.",
|
||||
deliveryTarget: {
|
||||
mode: "channel",
|
||||
channelId: "fixture-channel-id",
|
||||
replyToMessageId: "fixture-message-id",
|
||||
personaName: "Void",
|
||||
},
|
||||
priority: 0.9,
|
||||
mustEventuallyShare: false,
|
||||
createdAt: "2026-05-17T03:00:00.000Z",
|
||||
updatedAt: "2026-05-17T03:00:00.000Z",
|
||||
tags: ["fixture"],
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
mkdirSync(dirname(operationOutputPath), { recursive: true });
|
||||
writeFileSync(operationOutputPath, `${JSON.stringify(operations, null, 2)}\n`, "utf8");
|
||||
|
||||
if (lastMessagePath) {
|
||||
mkdirSync(dirname(resolve(lastMessagePath)), { recursive: true });
|
||||
writeFileSync(lastMessagePath, "fixture rumination queued one deliverable candidate\n", "utf8");
|
||||
}
|
||||
|
||||
process.stdout.write(JSON.stringify({ type: "fixture", operationCount: operations.length }) + "\n");
|
||||
});
|
||||
'@
|
||||
[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";
|
||||
|
||||
const statusDir = resolve(process.env.VOID_STATUS_DIR);
|
||||
const contentFileIndex = process.argv.indexOf("--content-file");
|
||||
const channelIndex = process.argv.indexOf("--channel-id");
|
||||
const replyIndex = process.argv.indexOf("--reply-to");
|
||||
const personaIndex = process.argv.indexOf("--persona-name");
|
||||
|
||||
if (contentFileIndex < 0 || channelIndex < 0) {
|
||||
console.error("fake sender expected --content-file and --channel-id");
|
||||
process.exit(2);
|
||||
}
|
||||
|
||||
const content = readFileSync(resolve(process.argv[contentFileIndex + 1]), "utf8");
|
||||
const channelId = process.argv[channelIndex + 1];
|
||||
const replyToMessageId = replyIndex >= 0 ? process.argv[replyIndex + 1] : undefined;
|
||||
const personaName = personaIndex >= 0 ? process.argv[personaIndex + 1] : undefined;
|
||||
const payload = {
|
||||
sentAt: "2026-05-17T03:01:00.000Z",
|
||||
mode: "channel",
|
||||
transport: personaName ? "webhook" : "bot",
|
||||
channelId,
|
||||
replyToMessageId,
|
||||
personaName,
|
||||
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");
|
||||
'@
|
||||
[System.IO.File]::WriteAllText($fakeSendPath, $fakeSendSource, [System.Text.UTF8Encoding]::new($false))
|
||||
|
||||
$cursorOperation = @{
|
||||
operation = "record_reviewed_messages"
|
||||
lastReviewedMessageId = "fixture-future-cursor"
|
||||
lastReviewedTimestamp = "2099-05-17T00:00:00.000Z"
|
||||
}
|
||||
$cursorOperation | ConvertTo-Json -Depth 32 | Set-Content -LiteralPath $cursorOperationPath -Encoding UTF8
|
||||
node .\scripts\void-self-state.mjs apply-operation --canonical $stateFilePath --operation-file $cursorOperationPath | Out-Null
|
||||
|
||||
$previousCodexExecutable = $env:CODEX_EXECUTABLE
|
||||
$previousCodexExecArgs = $env:CODEX_EXEC_ARGS
|
||||
$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
|
||||
|
||||
try {
|
||||
$env:CODEX_EXECUTABLE = "node"
|
||||
$env:CODEX_EXEC_ARGS = $fakeCodexPath
|
||||
$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
|
||||
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\run-void-moderator-rumination.ps1 -StateFilePath $stateFilePath | Out-Null
|
||||
} finally {
|
||||
$env:CODEX_EXECUTABLE = $previousCodexExecutable
|
||||
$env:CODEX_EXEC_ARGS = $previousCodexExecArgs
|
||||
$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
|
||||
}
|
||||
|
||||
$status = Get-Content -LiteralPath $statusPath -Raw -Encoding UTF8 | ConvertFrom-Json
|
||||
if ($status.status -ne "ok") {
|
||||
throw "Speech fixture did not finish ok."
|
||||
}
|
||||
if ([int]$status.proposedOperationCount -ne 1 -or [int]$status.appliedOperationCount -ne 2 -or [int]$status.deliveredCandidateCount -ne 1) {
|
||||
throw "Speech fixture expected one proposal, two applied operations, and one delivered candidate."
|
||||
}
|
||||
|
||||
$stateJson = node -e "const core=require('./packages/core/dist/index.js'); core.loadVoidSelfStateTypedDocuments({canonicalPath: process.argv[1]}).then((state)=>console.log(JSON.stringify(state))).catch((error)=>{ console.error(error); process.exit(1); })" $stateFilePath
|
||||
$state = $stateJson | ConvertFrom-Json
|
||||
$candidate = @($state.candidateInterventions.interventions)[0]
|
||||
$receipt = @($state.speechReceipts.recentReceipts)[0]
|
||||
|
||||
if ($candidate.status -ne "spoken") {
|
||||
throw "Speech fixture did not mark the candidate spoken."
|
||||
}
|
||||
if ($receipt.channelId -ne "fixture-channel-id" -or $receipt.replyToMessageId -ne "fixture-message-id") {
|
||||
throw "Speech fixture did not preserve the delivery receipt target."
|
||||
}
|
||||
|
||||
@{
|
||||
status = "ok"
|
||||
candidateStatus = $candidate.status
|
||||
receiptCount = @($state.speechReceipts.recentReceipts).Count
|
||||
deliveredCandidateCount = [int]$status.deliveredCandidateCount
|
||||
} | ConvertTo-Json -Compress
|
||||
} finally {
|
||||
Remove-Item -LiteralPath $tempRoot -Recurse -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
Loading…
Reference in a new issue