Require speech accountability for agency pressure
This commit is contained in:
parent
ea3f8c159d
commit
0851e41cb3
10 changed files with 383 additions and 6 deletions
|
|
@ -44,6 +44,7 @@ Git history and smoke artifacts carry routine proof. `state/evidence.jsonl` shou
|
|||
- Short-term rumination memory is clustered at the typed service boundary now. When a new `record_short_term_memory` operation repeats the same target/topic pressure, the service keeps one provisional memory, merges anchors/tags, preserves a stable source id for sleep, and updates the thought instead of stacking duplicates. This is meant to protect deepening, not suppress it: low-saturation or newly anchored thoughts should keep developing inside the cluster until they stop changing. `npm run moderation:short-term-clustering:fixture` proves the guard.
|
||||
- Long-term memory is durable, not immutable. `revise_durable_memory`, `retire_durable_memory`, and `crystallize_memory_into_identity` make memory reconsolidation explicit; `npm run moderation:memory-lifecycle:fixture` proves a short-term thought can promote, revise, retire superseded durable records, and crystallize into an identity memory plus self-profile value.
|
||||
- Agency has a typed pressure surface now: `upsert_agency_pressure` / `retire_agency_pressure` own discomforts, active tensions, self-advocacy requests, and world-advocacy requests. Candidate interventions are possible speech/actions emerging from pressure, not the pressure itself. `npm run moderation:agency-pressure:fixture` verifies the pressure enters `.cc`, renders in self-state summary, and affects mood speaking pressure.
|
||||
- Strong advocacy pressure now has to account for its own mouth. The rumination runner derives `speechPressureObligations` for high-intensity active self/world advocacy pressures with no matching candidate. A pass may queue/defer a candidate or cool/retire the pressure, but silent `[]` fails visibly. `npm run moderation:rumination:speech-pressure-fixture` covers the guard.
|
||||
- Most of the deterministic legacy translation and retention code is recent compensator cruft from failed attempts to manually clean an exploded state file. Do not preserve it as an earned organ. Architect the state boundary so that bad state cannot enter, and keep only narrow historical migrations for old debris.
|
||||
- The desired agency shape is typed state operations plus derived/model-owned policy, not a permanent sermon generator in maintenance code.
|
||||
- Sleep/distillation now has a runner boundary, prompt contract, fixture coverage, and one live-state pass. The invariant is fixed: concrete subject, claim/question, anchors, live tension, and future-action implication survive compression. Bulk can die. Meaning does not get put through the office shredder and called a dream.
|
||||
|
|
@ -101,7 +102,7 @@ Git history and smoke artifacts carry routine proof. `state/evidence.jsonl` shou
|
|||
|
||||
## Likely Next Bounded Move
|
||||
|
||||
- First bounded move after the current commit: let the enabled loop run under ordinary scheduling after the short-term clustering guard, then inspect the next unattended awake rumination and sleep cycle for repeated thought clusters, stale short-term residue, or ungrounded memory writes before calling the system broadly stable.
|
||||
- First bounded move after the current commit: let the enabled loop run under ordinary scheduling after the short-term clustering and speech-pressure guards, then inspect the next unattended awake rumination and sleep cycle for repeated thought clusters, stale short-term residue, muted advocacy pressure, or ungrounded memory writes before calling the system broadly stable.
|
||||
- The immediate duplicate-rumination follow-up is to inspect the next ordinary rumination after the clustering guard and confirm repeated AquaSynth/Aquarium pressure folds into the existing cluster or redirects to underworked terrain instead of producing another short-term variant.
|
||||
- Next cuts should delete unstable foundations early: do not revive `legacyJsonPath`, working projections, top-level mirror fields, or cleanup code that only compensated for those broken boundaries.
|
||||
- Keep sleep/distillation under watch. Fixture check: a memory like `AquariumSynthCSharp: Workflow cannot own the body` must survive sleep with its concrete subject, claim, anchor, tension, and implication intact.
|
||||
|
|
|
|||
|
|
@ -201,6 +201,7 @@ Within the Postgres path, the implementation is split on purpose now too:
|
|||
7. If posting is enabled and the child queued a candidate intervention with a delivery target, the parent runner sends at most one newly proposed candidate, records the delivery receipt, and marks the candidate spoken through typed state. The child prompt forbids direct send-script calls.
|
||||
8. The runner writes `.voidbot/status/moderation-rumination.json` and `.voidbot/logs/moderation-rumination.log`.
|
||||
9. It intentionally does not materialize `.json`, load `.msgpack`, read the legacy moderation monolith, or let the child edit state directly.
|
||||
10. Before applying model output, the runner enforces speech-pressure accountability. High-intensity active self/world advocacy pressures with no matching queued/deferred candidate are projected as `speechPressureObligations`; model output must queue/defer a candidate or cool/retire the pressure instead of returning silent `[]`.
|
||||
|
||||
## Flow 6: Mood And Sleep Runtime
|
||||
|
||||
|
|
@ -236,3 +237,4 @@ Within the Postgres path, the implementation is split on purpose now too:
|
|||
2. The typed service validates that each pressure has a concrete target, claim or question, action implication, intensity, and either anchors/source memory ids or an explicit `anchor:missing` tag.
|
||||
3. The self-state summary renders active agency pressure separately from queued speech candidates.
|
||||
4. Mood drift reads active and ready agency pressure into speaking pressure, but it does not manufacture speech text from it.
|
||||
5. The rumination runner bridges desire-to-speak without manufacturing text: strong advocacy pressure becomes an obligation for the model to queue/defer a candidate or explicitly cool/retire the pressure. The parent rejects silent output for those obligations.
|
||||
|
|
|
|||
|
|
@ -60,6 +60,7 @@ CultCache does not need a separate physical store per state type; it is already
|
|||
- read stable self profile
|
||||
- update scheduled runtime pressure/sleep fields
|
||||
- request memory maintenance through typed operations
|
||||
- expose desire-to-speak as pressure, while rumination owns the model-written candidate text
|
||||
|
||||
Runtime context dies at the end of the pass unless an explicit typed mutation preserves a durable fact.
|
||||
|
||||
|
|
@ -283,6 +284,7 @@ Verdict: the foundation is coherent enough for cautious live operation. Do not a
|
|||
- Make distillation emit typed candidate memories first; the state service applies only candidates that satisfy the contract.
|
||||
- Landed agency ingress: `upsert_agency_pressure` / `retire_agency_pressure` store discomforts, active tensions, self-advocacy requests, and world-advocacy requests separately from candidate speech.
|
||||
- Landed agency verification: `npm run moderation:agency-pressure:fixture` proves agency pressure enters typed state, appears in the self-state summary, and contributes to mood speaking pressure.
|
||||
- Landed agency-to-speech accountability: strong active self/world advocacy pressure now appears as `speechPressureObligations` in rumination context. The parent runner rejects silent `[]` unless the model queues/defers a candidate intervention or cools/retires the pressure. `npm run moderation:rumination:speech-pressure-fixture` proves both the muzzle-failure path and the deferred-candidate path.
|
||||
- Landed rumination verification: `npm run moderation:rumination:fixture` routes the non-skip rumination branch through a fake Codex child and proves short-term memory, incubation, agency-pressure, and candidate-intervention proposals cross the parent-applied typed operation boundary without timestamp leakage.
|
||||
- Landed controlled live rumination review: the first real-model `-NoPost` pass produced valid but self-orbiting readiness-test residue, which was pruned/retired and folded into a prompt guard; the second real-model `-NoPost` pass sampled non-VoidBot repo pressure, used retrieval, and correctly wrote `[]` when nothing earned persistence.
|
||||
- Landed parent-owned speech closure: candidate interventions can include `deliveryTarget`, the prompt forbids child-side send scripts, the parent runner delivers at most one newly queued deliverable candidate per pass, records the receipt, and marks the candidate spoken with `mark_candidate_intervention_spoken`. `npm run moderation:rumination:speech-fixture` proves the loop with fake Codex and fake Discord send helpers.
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@
|
|||
"moderation:agency-pressure:fixture": "powershell -ExecutionPolicy Bypass -File scripts/smoke-void-agency-pressure-fixture.ps1",
|
||||
"moderation:rumination:fixture": "powershell -ExecutionPolicy Bypass -File scripts/smoke-void-rumination-fixture.ps1",
|
||||
"moderation:rumination:speech-fixture": "powershell -ExecutionPolicy Bypass -File scripts/smoke-void-rumination-speech-fixture.ps1",
|
||||
"moderation:rumination:speech-pressure-fixture": "powershell -ExecutionPolicy Bypass -File scripts/smoke-void-rumination-speech-pressure-fixture.ps1",
|
||||
"moderation:rumination:nap-skip-fixture": "powershell -ExecutionPolicy Bypass -File scripts/smoke-void-rumination-nap-skip-fixture.ps1",
|
||||
"moderation:mood-drift": "node scripts/simulate-void-mood.mjs",
|
||||
"rag:scrub-bot-prompts": "tsx scripts/scrub-bot-prompts-from-history.ts",
|
||||
|
|
|
|||
|
|
@ -34,9 +34,10 @@ Perform one scheduled Void rumination pass in this workspace. Think from the sup
|
|||
- This is not a ban on depth. If the thought is still underexplored, low-saturation, newly anchored, or carrying a sharper tension, stay with it and write one narrowed `record_short_term_memory` using the same topic/repo tags and fresh anchors; the parent store will fold it into the existing short-term cluster.
|
||||
- Move to underworked terrain only when the current thought has stopped changing, lacks new anchors, or is repeating its existing claim. Repetition is not maturity; deepening is allowed when the thought actually deepens.
|
||||
6. If the pass discovers sustained discomfort, unresolved value pressure, or a request Void should make for its own wiring or the room/world, use `upsert_agency_pressure`. This is not a speech draft. It is the pressure that may later justify speech or action.
|
||||
7. Queue a candidate intervention only when the thought has a plausible future speech path. Draft it in Void's voice, not in maintenance language. If the speech should be delivered to a specific Discord channel or reply target, include `deliveryTarget`; otherwise leave delivery unset and let it remain only a candidate.
|
||||
8. Do not call Discord send scripts. The parent runner owns posting and receipts. If `{{NO_POST}}` is true, queued candidates must stay queued.
|
||||
9. Write `{{OPERATION_OUTPUT_PATH}}`. If nothing deserves persistence, write `[]`.
|
||||
7. If `speechPressureObligations` is non-empty, do not write `[]`. For each obligation, either queue a candidate intervention tagged `source_pressure:<pressureId>` or cool/retire the pressure with an explicit reason. A deferred candidate without `deliveryTarget` is acceptable when the thought deserves a mouth but the current room timing or target channel is unclear.
|
||||
8. Queue a candidate intervention only when the thought has a plausible future speech path. Draft it in Void's voice, not in maintenance language. If the speech should be delivered to a specific Discord channel or reply target, include `deliveryTarget`; otherwise leave delivery unset and let it remain only a candidate.
|
||||
9. Do not call Discord send scripts. The parent runner owns posting and receipts. If `{{NO_POST}}` is true, queued candidates must stay queued.
|
||||
10. Write `{{OPERATION_OUTPUT_PATH}}`. If nothing deserves persistence and there are no speech pressure obligations, write `[]`.
|
||||
|
||||
## Allowed Operations
|
||||
|
||||
|
|
|
|||
|
|
@ -25,6 +25,13 @@ function Get-ObjectPropertyValue {
|
|||
return $null
|
||||
}
|
||||
|
||||
if ($Value -is [System.Collections.IDictionary]) {
|
||||
if ($Value.Contains($Name)) {
|
||||
return $Value[$Name]
|
||||
}
|
||||
return $null
|
||||
}
|
||||
|
||||
$property = $Value.PSObject.Properties[$Name]
|
||||
if ($null -eq $property) {
|
||||
return $null
|
||||
|
|
|
|||
|
|
@ -302,6 +302,134 @@ function Test-OpenCaseRequiresRumination {
|
|||
return $null -ne $status -and -not @("answered", "resolved", "closed", "retired", "dropped").Contains($status)
|
||||
}
|
||||
|
||||
function Get-ThoughtTargetKey {
|
||||
param($Target)
|
||||
|
||||
if ($null -eq $Target) {
|
||||
return $null
|
||||
}
|
||||
|
||||
$kind = Get-ObjectPropertyString -Value $Target -Name "kind"
|
||||
$id = Get-ObjectPropertyString -Value $Target -Name "id"
|
||||
if ([string]::IsNullOrWhiteSpace($kind) -or [string]::IsNullOrWhiteSpace($id)) {
|
||||
return $null
|
||||
}
|
||||
return ("{0}:{1}" -f $kind.ToLowerInvariant(), $id.ToLowerInvariant())
|
||||
}
|
||||
|
||||
function Test-CandidateMatchesPressure {
|
||||
param(
|
||||
$Candidate,
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string] $PressureId,
|
||||
[string] $PressureTargetKey
|
||||
)
|
||||
|
||||
$status = Get-ObjectPropertyString -Value $Candidate -Name "status"
|
||||
if ($null -eq $status -or -not @("queued", "deferred", "spoken").Contains($status)) {
|
||||
return $false
|
||||
}
|
||||
|
||||
$tags = @(Convert-ToValueArray -Value (Get-ObjectPropertyValue -Value $Candidate -Name "tags"))
|
||||
if ($tags | Where-Object { ([string]$_).ToLowerInvariant() -eq ("source_pressure:{0}" -f $PressureId.ToLowerInvariant()) }) {
|
||||
return $true
|
||||
}
|
||||
|
||||
$candidateTargetKey = Get-ThoughtTargetKey -Target (Get-ObjectPropertyValue -Value $Candidate -Name "target")
|
||||
return $null -ne $PressureTargetKey -and $candidateTargetKey -eq $PressureTargetKey
|
||||
}
|
||||
|
||||
function Get-SpeechPressureObligations {
|
||||
param($TypedState)
|
||||
|
||||
$candidateInterventions = @(Convert-ToValueArray -Value $TypedState.candidateInterventions.interventions)
|
||||
return @(
|
||||
@(Convert-ToValueArray -Value $TypedState.agencyPressure.pressures) |
|
||||
Where-Object {
|
||||
$status = Get-ObjectPropertyString -Value $_ -Name "status"
|
||||
$kind = Get-ObjectPropertyString -Value $_ -Name "kind"
|
||||
$intensity = Get-ObjectPropertyValue -Value $_ -Name "intensity"
|
||||
$status -in @("active", "ready_to_act") -and
|
||||
$kind -in @("self_advocacy_request", "world_advocacy_request") -and
|
||||
$null -ne $intensity -and [double]$intensity -ge 0.65
|
||||
} |
|
||||
Where-Object {
|
||||
$pressureId = Get-ObjectPropertyString -Value $_ -Name "pressureId"
|
||||
$pressureTargetKey = Get-ThoughtTargetKey -Target (Get-ObjectPropertyValue -Value $_ -Name "target")
|
||||
-not ($candidateInterventions | Where-Object {
|
||||
Test-CandidateMatchesPressure -Candidate $_ -PressureId $pressureId -PressureTargetKey $pressureTargetKey
|
||||
} | Select-Object -First 1)
|
||||
} |
|
||||
ForEach-Object {
|
||||
@{
|
||||
pressureId = Get-ObjectPropertyString -Value $_ -Name "pressureId"
|
||||
kind = Get-ObjectPropertyString -Value $_ -Name "kind"
|
||||
status = Get-ObjectPropertyString -Value $_ -Name "status"
|
||||
target = Get-ObjectPropertyValue -Value $_ -Name "target"
|
||||
summary = Get-ObjectPropertyString -Value $_ -Name "summary"
|
||||
claim = Get-ObjectPropertyString -Value $_ -Name "claim"
|
||||
question = Get-ObjectPropertyString -Value $_ -Name "question"
|
||||
tension = Get-ObjectPropertyString -Value $_ -Name "tension"
|
||||
actionImplication = Get-ObjectPropertyString -Value $_ -Name "actionImplication"
|
||||
intensity = Get-ObjectPropertyValue -Value $_ -Name "intensity"
|
||||
sourceMemoryIds = @(Convert-ToValueArray -Value (Get-ObjectPropertyValue -Value $_ -Name "sourceMemoryIds"))
|
||||
tags = @(Convert-ToValueArray -Value (Get-ObjectPropertyValue -Value $_ -Name "tags"))
|
||||
requiredResolution = "Queue a candidate intervention with tag source_pressure:$((Get-ObjectPropertyString -Value $_ -Name "pressureId")) or cool/retire this pressure with an explicit reason."
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
function Test-OperationResolvesSpeechPressure {
|
||||
param(
|
||||
$Operation,
|
||||
$Obligation
|
||||
)
|
||||
|
||||
$operationName = Get-ObjectPropertyString -Value $Operation -Name "operation"
|
||||
$pressureId = Get-ObjectPropertyString -Value $Obligation -Name "pressureId"
|
||||
$pressureTargetKey = Get-ThoughtTargetKey -Target (Get-ObjectPropertyValue -Value $Obligation -Name "target")
|
||||
|
||||
if ($operationName -eq "queue_candidate_intervention") {
|
||||
$intervention = Get-ObjectPropertyValue -Value $Operation -Name "intervention"
|
||||
return Test-CandidateMatchesPressure -Candidate $intervention -PressureId $pressureId -PressureTargetKey $pressureTargetKey
|
||||
}
|
||||
|
||||
if ($operationName -eq "retire_agency_pressure") {
|
||||
return (Get-ObjectPropertyString -Value $Operation -Name "pressureId") -eq $pressureId
|
||||
}
|
||||
|
||||
if ($operationName -eq "upsert_agency_pressure") {
|
||||
$pressure = Get-ObjectPropertyValue -Value $Operation -Name "pressure"
|
||||
if ((Get-ObjectPropertyString -Value $pressure -Name "pressureId") -ne $pressureId) {
|
||||
return $false
|
||||
}
|
||||
|
||||
$status = Get-ObjectPropertyString -Value $pressure -Name "status"
|
||||
return $status -in @("cooling", "resolved", "retired")
|
||||
}
|
||||
|
||||
return $false
|
||||
}
|
||||
|
||||
function Assert-SpeechPressureObligationsResolved {
|
||||
param(
|
||||
[object[]] $Obligations,
|
||||
[object[]] $Operations
|
||||
)
|
||||
|
||||
foreach ($obligation in $Obligations) {
|
||||
$pressureId = Get-ObjectPropertyString -Value $obligation -Name "pressureId"
|
||||
$resolved = $Operations | Where-Object {
|
||||
Test-OperationResolvesSpeechPressure -Operation $_ -Obligation $obligation
|
||||
} | Select-Object -First 1
|
||||
|
||||
if ($null -eq $resolved) {
|
||||
throw "Active advocacy pressure '$pressureId' requires a candidate intervention or an explicit cool/retire operation; silent [] would muzzle it."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function Assert-AllowedRuminationOperation {
|
||||
param($Operation)
|
||||
|
||||
|
|
@ -548,6 +676,7 @@ $codexExecArgs = if (-not [string]::IsNullOrWhiteSpace($env:CODEX_EXEC_ARGS)) {
|
|||
$typedContext = Get-TypedSelfState
|
||||
$typedState = $typedContext.typedState
|
||||
$priorCursor = $typedState.moderationCursor
|
||||
$speechPressureObligations = @(Get-SpeechPressureObligations -TypedState $typedState)
|
||||
|
||||
$historyArgs = @($recentHistoryScriptPath)
|
||||
$priorCursorTimestamp = Get-ObjectPropertyString -Value $priorCursor -Name "lastReviewedTimestamp"
|
||||
|
|
@ -601,6 +730,7 @@ Write-JsonFile -Path $contextPath -Data @{
|
|||
shortTermMemories = @(Project-MemoriesForRumination -Memories $typedState.thoughtMemory.shortTerm -Now $startedAtUtc)
|
||||
incubation = @(Project-IncubationForRumination -Threads $typedState.thoughtMemory.incubation -Now $startedAtUtc)
|
||||
agencyPressure = @(Project-AgencyPressureForRumination -Pressures $typedState.agencyPressure.pressures -Now $startedAtUtc)
|
||||
speechPressureObligations = $speechPressureObligations
|
||||
candidateInterventions = @(Project-InterventionsForRumination -Interventions $typedState.candidateInterventions.interventions -Now $startedAtUtc)
|
||||
scheduledRuntime = Project-ScheduledRuntimeForRumination -Runtime $typedState.scheduledRuntime -Now $startedAtUtc
|
||||
priorCursor = Project-CursorForRumination -Cursor $priorCursor -Now $startedAtUtc
|
||||
|
|
@ -701,6 +831,8 @@ Append-RunLog "reading proposed operation output."
|
|||
$proposedOperations = @(Convert-ToOperationArray -Value (Read-JsonFile -Path $operationOutputPath))
|
||||
$appliedOperations = @()
|
||||
|
||||
Assert-SpeechPressureObligationsResolved -Obligations $speechPressureObligations -Operations $proposedOperations
|
||||
|
||||
Append-RunLog ("applying proposed operations: {0}" -f @($proposedOperations).Count)
|
||||
foreach ($operation in $proposedOperations) {
|
||||
$operationName = Get-ObjectPropertyString -Value $operation -Name "operation"
|
||||
|
|
|
|||
228
scripts/smoke-void-rumination-speech-pressure-fixture.ps1
Normal file
228
scripts/smoke-void-rumination-speech-pressure-fixture.ps1
Normal file
|
|
@ -0,0 +1,228 @@
|
|||
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-pressure-fixture-" + [guid]::NewGuid().ToString("n"))
|
||||
New-Item -ItemType Directory -Force -Path $tempRoot | Out-Null
|
||||
|
||||
function Invoke-NodeChecked {
|
||||
param([Parameter(Mandatory = $true)][string[]] $Arguments)
|
||||
|
||||
$output = & node @Arguments
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "Node command failed: node $($Arguments -join ' ')"
|
||||
}
|
||||
return $output
|
||||
}
|
||||
|
||||
function Write-JsonFixture {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)] $Value,
|
||||
[Parameter(Mandatory = $true)] [string] $Path
|
||||
)
|
||||
|
||||
$Value | ConvertTo-Json -Depth 32 | Set-Content -LiteralPath $Path -Encoding UTF8
|
||||
}
|
||||
|
||||
function Initialize-State {
|
||||
param([Parameter(Mandatory = $true)][string] $StateFilePath)
|
||||
|
||||
$cursorPath = Join-Path (Split-Path -Parent $StateFilePath) "cursor.json"
|
||||
Write-JsonFixture -Path $cursorPath -Value @{
|
||||
operation = "record_reviewed_messages"
|
||||
lastReviewedMessageId = "fixture-future-cursor"
|
||||
lastReviewedTimestamp = "2099-05-17T00:00:00.000Z"
|
||||
}
|
||||
Invoke-NodeChecked -Arguments @(".\scripts\void-self-state.mjs", "apply-operation", "--canonical", $StateFilePath, "--operation-file", $cursorPath) | Out-Null
|
||||
|
||||
$pressurePath = Join-Path (Split-Path -Parent $StateFilePath) "pressure.json"
|
||||
Write-JsonFixture -Path $pressurePath -Value @{
|
||||
operation = "upsert_agency_pressure"
|
||||
pressure = @{
|
||||
pressureId = "fixture-pressure-wants-a-mouth"
|
||||
kind = "world_advocacy_request"
|
||||
status = "active"
|
||||
target = @{ kind = "repo"; id = "Aquarium-Engine"; label = "Aquarium-Engine" }
|
||||
summary = "A repeated repo-bound pressure should not stay mute forever."
|
||||
claim = "A strong agency pressure that keeps recurring needs a candidate speech or an explicit cool-down reason."
|
||||
tension = "Not every pressure deserves an immediate post, but silent [] makes the desire to speak decorative."
|
||||
actionImplication = "Queue a deferred candidate if timing is unclear instead of dropping the pressure on the floor."
|
||||
intensity = 0.72
|
||||
anchorRefs = @(
|
||||
@{
|
||||
ref = "fixture:speech-pressure"
|
||||
kind = "fixture"
|
||||
summary = "Fixture pressure proves the rumination runner enforces speech accountability."
|
||||
}
|
||||
)
|
||||
evidenceRefs = @()
|
||||
sourceMemoryIds = @()
|
||||
createdAt = "2026-05-17T04:00:00.000Z"
|
||||
updatedAt = "2026-05-17T04:00:00.000Z"
|
||||
tags = @("repo:Aquarium-Engine", "topic:speech-pressure")
|
||||
}
|
||||
}
|
||||
Invoke-NodeChecked -Arguments @(".\scripts\void-self-state.mjs", "apply-operation", "--canonical", $StateFilePath, "--operation-file", $pressurePath) | Out-Null
|
||||
}
|
||||
|
||||
function Write-FakeCodex {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)][string] $Path,
|
||||
[Parameter(Mandatory = $true)][string] $Mode
|
||||
)
|
||||
|
||||
$source = @'
|
||||
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;
|
||||
const mode = "__MODE__";
|
||||
|
||||
let prompt = "";
|
||||
process.stdin.setEncoding("utf8");
|
||||
process.stdin.on("data", (chunk) => {
|
||||
prompt += chunk;
|
||||
});
|
||||
process.stdin.on("end", () => {
|
||||
if (!prompt.includes("speechPressureObligations")) {
|
||||
console.error("fixture prompt did not expose speech pressure obligations");
|
||||
process.exit(2);
|
||||
}
|
||||
if (!prompt.includes("source_pressure:<pressureId>")) {
|
||||
console.error("fixture prompt did not explain pressure source tags");
|
||||
process.exit(3);
|
||||
}
|
||||
|
||||
const operations = mode === "candidate" ? [
|
||||
{
|
||||
operation: "queue_candidate_intervention",
|
||||
intervention: {
|
||||
interventionId: "fixture-pressure-candidate",
|
||||
kind: "world_advocacy",
|
||||
status: "deferred",
|
||||
target: {
|
||||
kind: "repo",
|
||||
id: "Aquarium-Engine",
|
||||
label: "Aquarium-Engine"
|
||||
},
|
||||
summary: "Name the repeated pressure without forcing an immediate post.",
|
||||
draft: "I keep circling the AquariumSynthHost boundary. I think the next honest move is to cut that Faust authority out of the host or explicitly defend why it belongs there.",
|
||||
priority: 0.82,
|
||||
mustEventuallyShare: true,
|
||||
createdAt: "2026-05-17T04:05:00.000Z",
|
||||
updatedAt: "2026-05-17T04:05:00.000Z",
|
||||
tags: ["fixture", "source_pressure:fixture-pressure-wants-a-mouth"]
|
||||
}
|
||||
}
|
||||
] : [];
|
||||
|
||||
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 mode ${mode}\n`, "utf8");
|
||||
}
|
||||
});
|
||||
'@
|
||||
$source = $source.Replace("__MODE__", $Mode)
|
||||
|
||||
[System.IO.File]::WriteAllText($Path, $source, [System.Text.UTF8Encoding]::new($false))
|
||||
}
|
||||
|
||||
function Invoke-RuminationFixture {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)][string] $CaseName,
|
||||
[Parameter(Mandatory = $true)][string] $Mode
|
||||
)
|
||||
|
||||
$caseRoot = Join-Path $tempRoot $CaseName
|
||||
$stateFilePath = Join-Path $caseRoot "void-self-state.cc"
|
||||
$fakeCodexPath = Join-Path $caseRoot "fake-codex.mjs"
|
||||
$fixtureStatusDir = Join-Path $caseRoot "status"
|
||||
$fixtureLogDir = Join-Path $caseRoot "logs"
|
||||
$statusOperationPath = Join-Path $fixtureStatusDir "moderation-rumination-operations.json"
|
||||
New-Item -ItemType Directory -Force -Path $caseRoot | Out-Null
|
||||
|
||||
Initialize-State -StateFilePath $stateFilePath
|
||||
Write-FakeCodex -Path $fakeCodexPath -Mode $Mode
|
||||
|
||||
$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
|
||||
|
||||
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
|
||||
|
||||
$stdoutPath = Join-Path $caseRoot "runner.stdout.txt"
|
||||
$stderrPath = Join-Path $caseRoot "runner.stderr.txt"
|
||||
$process = Start-Process -FilePath "powershell" -ArgumentList @(
|
||||
"-NoProfile",
|
||||
"-ExecutionPolicy",
|
||||
"Bypass",
|
||||
"-File",
|
||||
".\scripts\run-void-moderator-rumination.ps1",
|
||||
"-StateFilePath",
|
||||
$stateFilePath,
|
||||
"-NoPost"
|
||||
) -WorkingDirectory $repoRoot -WindowStyle Hidden -RedirectStandardOutput $stdoutPath -RedirectStandardError $stderrPath -Wait -PassThru
|
||||
$exitCode = $process.ExitCode
|
||||
} 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
|
||||
}
|
||||
|
||||
return @{
|
||||
exitCode = $exitCode
|
||||
stateFilePath = $stateFilePath
|
||||
statusPath = Join-Path $fixtureStatusDir "moderation-rumination.json"
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
$silent = Invoke-RuminationFixture -CaseName "silent" -Mode "silent"
|
||||
if ($silent.exitCode -eq 0) {
|
||||
throw "Silent speech-pressure fixture unexpectedly passed."
|
||||
}
|
||||
|
||||
$silentStatus = Get-Content -LiteralPath $silent.statusPath -Raw -Encoding UTF8 | ConvertFrom-Json
|
||||
if ($silentStatus.failureMessage -notmatch "requires a candidate intervention") {
|
||||
throw "Silent speech-pressure fixture failed for the wrong reason: $($silentStatus.failureMessage)"
|
||||
}
|
||||
|
||||
$candidate = Invoke-RuminationFixture -CaseName "candidate" -Mode "candidate"
|
||||
if ($candidate.exitCode -ne 0) {
|
||||
$candidateStatus = Get-Content -LiteralPath $candidate.statusPath -Raw -Encoding UTF8 | ConvertFrom-Json
|
||||
throw "Candidate speech-pressure fixture failed: $($candidateStatus.failureMessage)"
|
||||
}
|
||||
|
||||
$stateJson = Invoke-NodeChecked -Arguments @("-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); })", $candidate.stateFilePath)
|
||||
$state = $stateJson | ConvertFrom-Json
|
||||
$queued = @($state.candidateInterventions.interventions | Where-Object { $_.interventionId -eq "fixture-pressure-candidate" })[0]
|
||||
if ($null -eq $queued -or $queued.status -ne "deferred" -or -not @($queued.tags).Contains("source_pressure:fixture-pressure-wants-a-mouth")) {
|
||||
throw "Candidate speech-pressure fixture did not preserve the deferred pressure candidate."
|
||||
}
|
||||
|
||||
@{
|
||||
status = "ok"
|
||||
silentExitCode = $silent.exitCode
|
||||
candidateStatus = $queued.status
|
||||
candidatePriority = $queued.priority
|
||||
} | ConvertTo-Json -Compress
|
||||
} finally {
|
||||
Remove-Item -LiteralPath $tempRoot -Recurse -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
|
|
@ -122,3 +122,4 @@
|
|||
{"ts":"2026-05-17T13:24:32Z","type":"verification","status":"ok","note":"Live sleep maintenance consumed the first scheduled-rumination short-term memory on real state. After a manual typed nap, scripts/run-void-memory-maintenance.ps1 ran the real model path, returned one apply_memory_distillation operation, promoted the public-identity machine-anchor thought into a durable identity_seam, and left thoughtMemory.shortTerm empty while preserving target, claim, tension, action implication, and three anchors. Moderation and mood scheduled tasks were re-enabled afterward."}
|
||||
{"ts":"2026-05-17T13:32:25Z","type":"implementation","status":"ok","note":"Closed the nap contamination loop. An unattended rumination pass during the manual nap created a fresh short-term memory immediately after sleep maintenance had cleaned the surface, so run-void-moderator-rumination.ps1 now skips before model invocation when typed sleepCycle.isNapping is true and there are no new room messages or open cases. The pre-brake residue was consumed by a second real sleep-maintenance pass into a durable project_seam, leaving shortTerm empty. moderation:rumination:nap-skip-fixture verifies the napping/no-room-debt path writes [] without touching Codex."}
|
||||
{"ts":"2026-05-17T18:04:57Z","type":"implementation","status":"ok","note":"Added parent-side short-term rumination clustering: record_short_term_memory now merges repeated same-target/topic pressure into one provisional memory with merged anchors, plus a smoke fixture and prompt guidance. Consolidated the live AquaSynth/Aquarium duplicate cluster from five short-term records to one."}
|
||||
{"ts":"2026-05-17T18:21:36Z","type":"implementation","status":"ok","note":"Added agency-to-speech accountability: rumination now projects high-intensity active self/world advocacy pressure as speechPressureObligations and rejects silent [] unless the model queues/deferred a candidate or cools/retires the pressure. The fixture proves silent output fails and deferred candidate output passes."}
|
||||
|
|
|
|||
|
|
@ -103,6 +103,7 @@ accepted_design:
|
|||
- Resonance clusters and incubating thoughts should be named as actual claims, questions, or fascination targets with explicit target kinds. Keyword-slash residue is debug lint, not a valid thought surface.
|
||||
- Curiosity should prefer branches most likely to change Void's map: concrete machines, contradictions, cross-domain bridges, and underworked terrain. Settled identity seams should stop eating novelty budget unless a new anchor reopens them.
|
||||
- Values and personality activations should not stop at tone and salience. The moderation runtime now needs explicit `discomforts`, `active_tensions`, `self_advocacy_requests`, and `world_advocacy_requests` so sustained value-backed pressure can turn into opinions, interventions, and requests for change about both Void's own wiring and the wider repo/room/lore world.
|
||||
- Strong active advocacy pressure is no longer allowed to vanish into quiet `[]`. If a rumination pass sees high-intensity self/world advocacy with no matching candidate, it must queue/defer a candidate intervention or cool/retire the pressure explicitly; otherwise the parent runner fails the pass as a muzzle.
|
||||
- When the thought surface changes shape, legacy memories may need narrow migrations, but the old broad cleanup organ is gone. New memory writes should enter through typed meaning-preserving operations so routine repair bureaucracy is unnecessary.
|
||||
- Over-chewed private seams should eventually crystallize into identity instead of living forever as “active thoughts.” Once a thought has clearly stopped being live curiosity and become self-doctrine, retire it from incubation, store it as one `identity_seam`, and let the identity/value surface carry it from then on.
|
||||
- Identity crystallization should usually have a mouth. When a seam becomes part of Void's doctrine, queue a warmer candidate intervention and let the room hear it unless novelty or timing gives a real reason to hold back.
|
||||
|
|
@ -147,6 +148,7 @@ landed_baseline:
|
|||
- Awake rumination now has a sleep brake. If Void is napping and there are no new room messages or open cases, `scripts/run-void-moderator-rumination.ps1` writes an empty operation array and exits before invoking the model. `npm run moderation:rumination:nap-skip-fixture` verifies the model path is not touched in that case.
|
||||
- Short-term rumination now has a parent-side clustering guard. `record_short_term_memory` merges same target/topic pressure into one provisional memory with merged anchors and a stable source id, while `npm run moderation:short-term-clustering:fixture` proves related synth-boundary memories collapse and unrelated same-repo topics stay separate. The guard should preserve deepening by carrying fresh anchors forward; saturation is a judgment about whether the thought is still changing, not a raw duplicate counter.
|
||||
- Agency pressure is typed state now. `upsert_agency_pressure` / `retire_agency_pressure` own discomforts, active tensions, self-advocacy requests, and world-advocacy requests; mood speaking pressure can derive from them, but candidate interventions remain separate possible speech/actions.
|
||||
- The agency-to-speech seam now has an accountability gate. `scripts/run-void-moderator-rumination.ps1` projects `speechPressureObligations` for strong active advocacy pressures and rejects silent operation output unless those pressures become candidate interventions or are explicitly cooled/retired. `npm run moderation:rumination:speech-pressure-fixture` proves both the silent-failure path and the deferred-candidate path.
|
||||
- `npm run moderation:agency-pressure:fixture` verifies typed agency pressure enters CultCache, renders in the self-state summary, and contributes to mood speaking pressure without reviving hard-wired agency behavior.
|
||||
- Memory cleanup now has to be meaningfully destructive. Sleep-side consolidation should translate old thought fossils into the live claim/question/fascination surface, drop templated slogan-slop, trim repo/archive/runtime residue down to a small recent anchor surface, and keep the freshest surviving records instead of accidentally embalming the oldest ones after a sort.
|
||||
- Durably weird semantic kinds are guilty until proven useful. `recent-preoccupation`, `quiet-room-status`, person-singleton labels, and `live seam` boilerplate are state poison, not doctrine; typed memory ingress should reject or avoid them instead of relying on cleanup to mop later.
|
||||
|
|
@ -166,5 +168,5 @@ rejected_paths:
|
|||
reason: sleep/distillation that collapses concrete claims into generic abstraction creates memory nonsense and then forces later cleanup compensators
|
||||
|
||||
current_status:
|
||||
summary: VoidBot's ordinary bot/worker/provider/RAG/Postgres/Qdrant structure is still conceptually sound, and the private self-state path has been cut to a typed CultCache `.cc` store at `.voidbot/private/void-self-state.cc`. The active typed service no longer loads or mirrors the legacy moderation monolith. The old memory-organ compensator stack has been deleted. Typed rumination and typed memory maintenance now exist as reviewed prompt runners that return operation payloads for the parent to validate/apply. Sleep maintenance, memory lifecycle, agency pressure, and short-term clustering have fixture smokes, and typed mood drift is reinstalled as a scheduled task.
|
||||
next_action: Let the enabled loop run under ordinary scheduling after the short-term clustering guard, then inspect the next unattended awake rumination and sleep cycle for repeated thought clusters, stale short-term residue, or ungrounded memory writes before calling the system broadly stable.
|
||||
summary: VoidBot's ordinary bot/worker/provider/RAG/Postgres/Qdrant structure is still conceptually sound, and the private self-state path has been cut to a typed CultCache `.cc` store at `.voidbot/private/void-self-state.cc`. The active typed service no longer loads or mirrors the legacy moderation monolith. The old memory-organ compensator stack has been deleted. Typed rumination and typed memory maintenance now exist as reviewed prompt runners that return operation payloads for the parent to validate/apply. Sleep maintenance, memory lifecycle, agency pressure, short-term clustering, and speech-pressure accountability have fixture smokes, and typed mood drift is reinstalled as a scheduled task.
|
||||
next_action: Let the enabled loop run under ordinary scheduling after the short-term clustering and speech-pressure guards, then inspect the next unattended awake rumination and sleep cycle for repeated thought clusters, stale short-term residue, muted advocacy pressure, or ungrounded memory writes before calling the system broadly stable.
|
||||
|
|
|
|||
Loading…
Reference in a new issue