From e3735fb9c4d1f87c3ec13d8ba088792fb9ec94da Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Sun, 17 May 2026 09:45:39 +0100 Subject: [PATCH] Add typed memory maintenance runner --- README.md | 5 +- notes/fresh-workspace-handoff.md | 3 +- notes/voidbot-current-system-map.md | 14 +- notes/voidbot-implementation-plan.md | 9 +- package.json | 1 + prompts/void-memory-maintenance.md | 67 ++++ scripts/run-void-memory-maintenance.ps1 | 446 ++++++++++++++++++++++++ state/evidence.jsonl | 1 + state/map.yaml | 5 +- 9 files changed, 542 insertions(+), 9 deletions(-) create mode 100644 prompts/void-memory-maintenance.md create mode 100644 scripts/run-void-memory-maintenance.ps1 diff --git a/README.md b/README.md index 5e558df..771b283 100644 --- a/README.md +++ b/README.md @@ -567,6 +567,7 @@ Tracked prompt surfaces: - `config/discord-server-rules.md` - `config/moderation-review-agent.md` +- `prompts/void-memory-maintenance.md` - `prompts/void-moderator-rumination.md` - `styles/void-default.md` @@ -575,6 +576,8 @@ Local moderation-state surfaces: - `.voidbot/private/void-self-state.cc`: canonical typed CultCache self-state - `.voidbot/status/moderation-rumination-context.json`: bounded prompt-facing run context - `.voidbot/status/moderation-rumination-operations.json`: child-proposed typed operation payloads +- `.voidbot/status/void-memory-maintenance-context.json`: bounded prompt-facing memory-maintenance context +- `.voidbot/status/void-memory-maintenance-operations.json`: child-proposed memory/incubation/candidate operations Chronological recent-history helper: @@ -630,7 +633,7 @@ That gives the loop a compact digest of recent commit motion across every tracke The moderator state is now typed CultCache state, not an editable Ghostlight-shaped JSON projection. The typed documents own profile, moderation cursor/open cases, speech receipts, thought memory/incubation, scheduled runtime, and candidate interventions. The old JSON projection, state template, and context exporter are gone. The typed rumination runner can still think about GameCult projects, archived Discord seams, indexed repos, recent cross-repo commit motion, and lore, but durable changes must come back as typed operation payloads. The child does not edit state. -The mood-drift task is currently a typed sleep/speaking-pressure pass. Memory maintenance, dream residue, identity crystallization, value pressure, and other agency behavior have to be rebuilt as typed operation proposals before they come back. No compatibility séance. +The mood-drift task is currently a typed sleep/speaking-pressure pass. Memory maintenance now has its first replacement runner at `scripts/run-void-memory-maintenance.ps1`: it reads typed self-state, loads `prompts/void-memory-maintenance.md`, and applies only memory/incubation/candidate typed operation proposals. Use `-StateFilePath` for fixture stores. Dream residue, identity crystallization, value pressure, and other agency behavior still have to come back through that operation boundary. No compatibility séance. Directly invoked live replies read the same typed private self-state as a distilled self-state attachment. When the sleep projection says Void is napping, the direct reply path skips the expensive room-read sidecar, avoids normal attentive service-work, and answers in brief sleepy grumbles instead. The moderation loop now speaks through the local bot token instead of approval-gated side-effecting MCP tools, so unattended runs can DM or post without making Codex stop and ask your permission every time it finds a mouthful of words. diff --git a/notes/fresh-workspace-handoff.md b/notes/fresh-workspace-handoff.md index e5ed88f..a923062 100644 --- a/notes/fresh-workspace-handoff.md +++ b/notes/fresh-workspace-handoff.md @@ -33,9 +33,10 @@ Git history and smoke artifacts carry routine proof. `state/evidence.jsonl` shou - Prompt-facing rumination chronology is projected by `scripts/lib/void-rumination-context-projection.ps1`. Exact timestamps stay in typed state/status/cursors; the child context gets relative phrases like `5 minutes ago`, including recent Discord messages, repo activity, receipts, memory timestamps, and sleep/speech timing. - `scripts/simulate-void-mood.mjs` has been rebuilt as a typed-only sleep/speaking-pressure pass over `.voidbot/private/void-self-state.cc`. - The old memory-organ script family, legacy moderation-state template, and legacy moderation context exporter have been deleted. Do not revive their cleanup, agency, dream, vector-incubation, or mirror-repair behavior as compatibility scaffolding. +- `scripts/run-void-memory-maintenance.ps1` is the first replacement memory-maintenance organ. It builds a bounded context from typed memories/incubation/candidates/runtime, loads `prompts/void-memory-maintenance.md`, and applies only allowed memory/incubation/candidate typed operation proposals through `scripts/void-self-state.mjs`. - 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 must be rewritten with a meaning-preservation contract: concrete subject, claim/question, evidence refs, live tension, and future-action implication survive compression. Bulk can die. Meaning does not get put through the office shredder and called a dream. +- Sleep/distillation now has a runner boundary and prompt contract, but the substantive model behavior still needs live exercising. The invariant is fixed: concrete subject, claim/question, evidence refs, live tension, and future-action implication survive compression. Bulk can die. Meaning does not get put through the office shredder and called a dream. - Typed memory operations now enforce the first ingress wall for the next brain: `append_distilled_memory`, `propose_memory_distillation`, and `apply_memory_distillation` require a concrete target, claim or question, tension, action implication, and evidence refs unless the operation explicitly tags `evidence:missing`. This does not fix old state. It prevents new summary-only mush from being accepted as memory. - The owner Codex lane is no longer one swollen file; orchestration, runtime/parsing, rendering, and shared helpers are split under `packages/providers/src/owner-codex-*.ts`. - The local Ollama lane is no longer one swollen file; orchestration stays in `packages/providers/src/local-llm-provider.ts` while prompt/rendering, tool-loop helpers, and shared types/constants live under `packages/providers/src/local-llm-*.ts`. diff --git a/notes/voidbot-current-system-map.md b/notes/voidbot-current-system-map.md index c358cfc..1526f47 100644 --- a/notes/voidbot-current-system-map.md +++ b/notes/voidbot-current-system-map.md @@ -120,6 +120,8 @@ This note is the source-grounded description of how the live VoidBot stack is sh - rumination context projector. It turns typed timestamps and helper payloads into prompt-facing relative phrases while leaving exact chronology in parent-owned typed state, status, and cursor bookkeeping. - `scripts/void-self-state.mjs` - typed self-state operation CLI. It applies strict operation payloads such as cursor updates, open-case changes, delivery receipts, repo cursor updates, memory appends, incubation merges, candidate interventions, sleep-cycle updates, and speaking-pressure updates through `packages/core/src/void-self-state-service.ts` against the CultCache `.cc` store. +- `scripts/run-void-memory-maintenance.ps1` + - typed memory-maintenance runner. It builds a bounded prompt-facing context from typed memories, incubation, candidates, receipts, and scheduled runtime; loads `prompts/void-memory-maintenance.md`; asks Codex for memory/incubation/candidate operation proposals; then applies only allowed typed operations through `scripts/void-self-state.mjs`. It accepts `-StateFilePath` so fixture passes can use a throwaway `.cc` store. - `scripts/install-moderation-rumination-task.ps1` - installs the local 15-minute scheduled task that runs the moderation/participation loop through the hidden PowerShell launcher shim. - `scripts/simulate-void-mood.mjs` @@ -183,4 +185,14 @@ Within the Postgres path, the implementation is split on purpose now too: 2. `scripts/simulate-void-mood.mjs` reads typed documents from `.voidbot/private/void-self-state.cc`. 3. It updates `void.scheduled_runtime.sleepCycle` and `void.scheduled_runtime.speakingPressure` through `update_sleep_cycle` and `update_speaking_pressure` operations. 4. It writes `.voidbot/status/void-mood-drift.json`. -5. The old memory-organ script family, legacy state template, and legacy context exporter have been deleted. Memory maintenance, incubation, dream residue, and agency must be rebuilt as typed operation proposals with meaning-preserving contracts rather than revived from the compensator stack. +5. The old memory-organ script family, legacy state template, and legacy context exporter have been deleted. + +## Flow 7: Typed Memory Maintenance + +1. `scripts/run-void-memory-maintenance.ps1` exits if moderation rumination is holding its lock. +2. It reads `.voidbot/private/void-self-state.cc` through the typed self-state service. +3. It writes `.voidbot/status/void-memory-maintenance-context.json` with prompt-facing relative chronology and only typed memory/incubation/candidate surfaces. +4. It loads `prompts/void-memory-maintenance.md` and asks Codex for operation proposals unless `-SkipModel` is set. +5. The parent runner rejects any operation outside memory, incubation, or candidate-intervention maintenance. +6. The parent runner applies allowed operations through `scripts/void-self-state.mjs`. +7. This is the replacement path for sleep/distillation and agency work: model-owned proposals crossing the typed operation boundary, not deterministic repair code editing state. diff --git a/notes/voidbot-implementation-plan.md b/notes/voidbot-implementation-plan.md index bd7ebec..b61fdcf 100644 --- a/notes/voidbot-implementation-plan.md +++ b/notes/voidbot-implementation-plan.md @@ -164,7 +164,7 @@ The runner can hand these to a CLI/MCP tool. The store decides what survives. - the old memory-organ script family, legacy moderation state template, and legacy context exporter have now been deleted instead of preserved as forensic bait - deterministic cleanup is mostly recent scar tissue from failed manual cleanup passes; it should stay deleted rather than be ported unless a future typed operation protects a real invariant in the new model - hard-wired agency policy is mostly current scaffolding, not the final authority; identity, advocacy, and speech candidates should emerge from typed memory/state operations plus model-owned judgment under validation -- sleep/distillation currently lacks a hard meaning-preservation implementation in the active runner, so it remains offline until rebuilt +- sleep/distillation now has a typed maintenance runner and reviewed prompt boundary, but still needs real model-pass hardening before it should be scheduled unattended Verdict: stop feature work and rebuild this foundation. The rest of the machine can keep running, but moderation/mood/self-state work should cut toward typed state tools before adding new behavior. @@ -239,8 +239,9 @@ Verdict: stop feature work and rebuild this foundation. The rest of the machine ### Commit 9: Replace Compensating Cleanup With Meaning-Preserving Sleep -- Delete cleanup paths whose only job was repairing JSON projection edits, legacy mirrors, keyword sludge, or overgrown runtime residue. -- Delete or demote hard-wired agency paths whose real job is turning current heuristic scores into doctrine, advocacy, or speech candidates without an explicit typed-state contract. +- Landed cut: cleanup paths whose only job was repairing JSON projection edits, legacy mirrors, keyword sludge, or overgrown runtime residue have been deleted with the old memory-organ stack. +- Landed cut: hard-wired agency paths that turned heuristic scores into doctrine, advocacy, or speech candidates without an explicit typed-state contract have been deleted. +- Landed replacement boundary: `scripts/run-void-memory-maintenance.ps1` asks the model for memory/incubation/candidate typed operation proposals using `prompts/void-memory-maintenance.md`, rejects non-maintenance operations, and applies the rest through `scripts/void-self-state.mjs`. - Treat legacy brain mush as a migration problem, not the design center. New memory writes must cross the meaning-preserving typed operation schema before they reach CultCache. - Redesign sleep/distillation around an explicit contract: - preserve the claim/question/fascination target @@ -250,7 +251,7 @@ Verdict: stop feature work and rebuild this foundation. The rest of the machine - preserve why the memory should affect future action - never replace a concrete thought with a prettier generic principle unless the operation records what was lost and why that loss is acceptable - Make distillation emit typed candidate memories first; the state service applies only candidates that satisfy the contract. -- Verification: fixture memories such as `AquariumSynthCSharp: Workflow cannot own the body` survive sleep with their subject, claim, evidence, and tension intact. +- Remaining verification: model-pass fixtures such as `AquariumSynthCSharp: Workflow cannot own the body` survive sleep with their subject, claim, evidence, and tension intact. ### Commit 10: Resume Product Hardening diff --git a/package.json b/package.json index 34f58df..ea78de2 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "moderation:recent-history": "node scripts/export-recent-discord-history.mjs", "moderation:random-history": "node scripts/export-random-discord-history.mjs", "moderation:recent-repo-activity": "node scripts/export-recent-repo-activity.mjs", + "moderation:memory-maintenance": "powershell -ExecutionPolicy Bypass -File scripts/run-void-memory-maintenance.ps1", "moderation:mood-drift": "node scripts/simulate-void-mood.mjs", "rag:scrub-bot-prompts": "tsx scripts/scrub-bot-prompts-from-history.ts", "rag:index-sources": "tsx scripts/index-source-repos.ts", diff --git a/prompts/void-memory-maintenance.md b/prompts/void-memory-maintenance.md new file mode 100644 index 0000000..5489de8 --- /dev/null +++ b/prompts/void-memory-maintenance.md @@ -0,0 +1,67 @@ +# Void Memory Maintenance + +Perform one private Void memory-maintenance pass in this workspace. This is not +room moderation and not a speech pass. Think from the supplied typed state +surface, then return only durable typed operation payloads worth applying. + +## Required Reading + +- `{{CONTEXT_PATH}}` +- `styles/void-default.md` + +## State Boundary + +- The canonical private self-state is `{{STATE_FILE_PATH}}`. +- The private self-state is a typed CultCache `.cc` store. +- Do not read or write legacy moderation projection files. +- Do not edit tracked repo files. +- Do not mutate self-state directly. +- Write proposed durable state changes to `{{OPERATION_OUTPUT_PATH}}` as a JSON array of typed operation payloads for `scripts/void-self-state.mjs apply-operation`. + +## Purpose + +Sleep and memory maintenance are allowed to reduce bulk, not meaning. + +A valid memory/distillation operation must preserve: + +- the concrete target +- the claim or question +- the live tension or counterweight +- why this should affect future action +- evidence refs, or an explicit `evidence:missing` tag + +If those pieces cannot be preserved, do not invent a prettier abstraction. Let the +thought cool, merge support into incubation, or write nothing. + +## Allowed Operations + +- `append_distilled_memory` +- `merge_incubation_support` +- `queue_candidate_intervention` +- `retire_candidate_intervention` +- `propose_memory_distillation` +- `apply_memory_distillation` + +Do not emit cursor, receipt, sleep-cycle, or speaking-pressure operations. + +## Pass Shape + +1. Read the context file. +2. If there are no memories, no incubation threads, and no candidate interventions needing attention, write `[]`. +3. Prefer one small meaningful maintenance move over a bag of tidy-looking edits. +4. If two memories really collapse into one stronger memory, use `apply_memory_distillation`. +5. If a thought is still alive but undercooked, use `merge_incubation_support`. +6. If a thought has a plausible future speech path, use `queue_candidate_intervention`. +7. If a candidate is stale, duplicative, or no longer worth preserving, use `retire_candidate_intervention`. +8. Write `{{OPERATION_OUTPUT_PATH}}`. If nothing deserves persistence, write `[]`. + +## Voice Discipline + +- Do not write template language into memory. +- Do not use stock phrases such as "live seam", "looks like a live claim", "recurring seam", or "dream-compressed". +- Do not save a thought merely because it fits the operation shape. +- The operation schema is the storage contract, not Void's voice. + +## Completion + +At the end, print a short run summary. diff --git a/scripts/run-void-memory-maintenance.ps1 b/scripts/run-void-memory-maintenance.ps1 new file mode 100644 index 0000000..8e2c899 --- /dev/null +++ b/scripts/run-void-memory-maintenance.ps1 @@ -0,0 +1,446 @@ +param( + [string] $StateFilePath, + [switch] $SkipModel +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = "Stop" +$ProgressPreference = "SilentlyContinue" + +$repoRoot = Split-Path -Parent $PSScriptRoot +Set-Location -LiteralPath $repoRoot + +$stateFilePath = if ([string]::IsNullOrWhiteSpace($StateFilePath)) { + Join-Path $repoRoot ".voidbot\private\void-self-state.cc" +} else { + $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($StateFilePath) +} +$statusDir = Join-Path $repoRoot ".voidbot\status" +$logDir = Join-Path $repoRoot ".voidbot\logs" +$statusPath = Join-Path $statusDir "void-memory-maintenance.json" +$summaryLogPath = Join-Path $logDir "void-memory-maintenance.log" +$lastMessagePath = Join-Path $statusDir "void-memory-maintenance-last-message.txt" +$operationOutputPath = Join-Path $statusDir "void-memory-maintenance-operations.json" +$contextPath = Join-Path $statusDir "void-memory-maintenance-context.json" +$lockPath = Join-Path $statusDir "void-memory-maintenance.lock" +$moderationLockPath = Join-Path $statusDir "moderation-rumination.lock" +$promptTemplatePath = Join-Path $repoRoot "prompts\void-memory-maintenance.md" +$contextProjectionScriptPath = Join-Path $repoRoot "scripts\lib\void-rumination-context-projection.ps1" +$selfStateScriptPath = Join-Path $repoRoot "scripts\void-self-state.mjs" +$startedAtUtc = [DateTime]::UtcNow + +. $contextProjectionScriptPath + +function Write-JsonFile { + param( + [Parameter(Mandatory = $true)] + [string] $Path, + [Parameter(Mandatory = $true)] + $Data + ) + + $directory = Split-Path -Parent $Path + if (-not [string]::IsNullOrWhiteSpace($directory)) { + New-Item -ItemType Directory -Force -Path $directory | Out-Null + } + + $json = if ($Data -is [System.Array] -and $Data.Count -eq 0) { + "[]" + } else { + $Data | ConvertTo-Json -Depth 32 + } + $utf8NoBom = [System.Text.UTF8Encoding]::new($false) + [System.IO.File]::WriteAllText($Path, $json, $utf8NoBom) +} + +function Read-JsonFile { + param([Parameter(Mandatory = $true)][string] $Path) + + if (-not (Test-Path $Path)) { + return $null + } + + $raw = Get-Content -Path $Path -Raw -Encoding UTF8 + if ([string]::IsNullOrWhiteSpace($raw)) { + return $null + } + + return $raw | ConvertFrom-Json +} + +function Read-TextFile { + param([Parameter(Mandatory = $true)][string] $Path) + return Get-Content -Path $Path -Raw -Encoding UTF8 +} + +function Append-RunLog { + param([Parameter(Mandatory = $true)][string] $Line) + $timestamped = "[{0}] {1}" -f ([DateTime]::UtcNow.ToString("o")), $Line + Add-Content -Path $summaryLogPath -Value $timestamped -Encoding UTF8 +} + +function Read-DotEnv { + param([Parameter(Mandatory = $true)][string] $Path) + + $values = @{} + if (-not (Test-Path $Path)) { + return $values + } + + foreach ($line in Get-Content -Path $Path -Encoding UTF8) { + $trimmed = $line.Trim() + if ($trimmed.Length -eq 0 -or $trimmed.StartsWith("#")) { + continue + } + + $separator = $trimmed.IndexOf("=") + if ($separator -lt 0) { + continue + } + + $key = $trimmed.Substring(0, $separator).Trim() + $value = $trimmed.Substring($separator + 1).Trim() + if ($value.Length -ge 2 -and (($value.StartsWith('"') -and $value.EndsWith('"')) -or ($value.StartsWith("'") -and $value.EndsWith("'")))) { + $value = $value.Substring(1, $value.Length - 2) + } + $values[$key] = $value + } + + return $values +} + +function Split-CommandArgs { + param([string] $Value) + + if ([string]::IsNullOrWhiteSpace($Value)) { + return @() + } + + return [regex]::Matches($Value, '("[^"]*"|''[^'']*''|\S+)') | + ForEach-Object { $_.Value.Trim().Trim("'`"") } +} + +function Quote-ProcessArgument { + param([Parameter(Mandatory = $true)][string] $Value) + + if ($Value -notmatch '[\s"]') { + return $Value + } + + $escaped = $Value -replace '(\\*)"', '$1$1\"' + $escaped = $escaped -replace '(\\+)$', '$1$1' + return '"' + $escaped + '"' +} + +function Invoke-CodexExec { + param( + [Parameter(Mandatory = $true)] + [string] $Executable, + [Parameter(Mandatory = $true)] + [string[]] $Arguments, + [Parameter(Mandatory = $true)] + [string] $WorkingDirectory, + [Parameter(Mandatory = $true)] + [string] $InputText + ) + + $startInfo = [System.Diagnostics.ProcessStartInfo]::new() + $startInfo.FileName = $Executable + $startInfo.WorkingDirectory = $WorkingDirectory + $startInfo.UseShellExecute = $false + $startInfo.RedirectStandardInput = $true + $startInfo.RedirectStandardOutput = $true + $startInfo.RedirectStandardError = $true + $startInfo.CreateNoWindow = $true + $utf8NoBom = [System.Text.UTF8Encoding]::new($false) + $startInfo.StandardOutputEncoding = $utf8NoBom + $startInfo.StandardErrorEncoding = $utf8NoBom + $startInfo.Arguments = [string]::Join(" ", ($Arguments | ForEach-Object { Quote-ProcessArgument -Value $_ })) + + $process = [System.Diagnostics.Process]::new() + $process.StartInfo = $startInfo + + try { + [void]$process.Start() + $inputWriter = [System.IO.StreamWriter]::new($process.StandardInput.BaseStream, $utf8NoBom, 4096, $true) + + try { + $inputWriter.Write($InputText) + $inputWriter.Flush() + } finally { + $inputWriter.Dispose() + $process.StandardInput.Close() + } + + $stdoutTask = $process.StandardOutput.ReadToEndAsync() + $stderrTask = $process.StandardError.ReadToEndAsync() + + $process.WaitForExit() + $stdoutTask.Wait() + $stderrTask.Wait() + + return @{ + ExitCode = $process.ExitCode + StdOut = $stdoutTask.Result + StdErr = $stderrTask.Result + } + } finally { + $process.Dispose() + } +} + +function Invoke-NodeJson { + param([Parameter(Mandatory = $true)][string[]] $Arguments) + + $output = & node @Arguments + if ($LASTEXITCODE -ne 0) { + throw "Node helper failed: node $($Arguments -join ' ')" + } + if ([string]::IsNullOrWhiteSpace($output)) { + return $null + } + return $output | ConvertFrom-Json +} + +function Get-TypedSelfState { + $script = @" +const core = require('./packages/core/dist'); +Promise.all([ + core.loadVoidSelfStateTypedDocuments({ canonicalPath: process.argv[1] }), + core.loadVoidSelfState(process.argv[1]), +]).then(([typedState, context]) => { + console.log(JSON.stringify({ typedState, summary: context?.summary ?? '' })); +}).catch((error) => { console.error(error); process.exit(1); }); +"@ + $output = & node -e $script $stateFilePath + if ($LASTEXITCODE -ne 0) { + throw "Failed to read typed self-state." + } + return $output | ConvertFrom-Json +} + +function Apply-TypedOperation { + param([Parameter(Mandatory = $true)] $Operation) + + $operationInputPath = Join-Path $statusDir ("void-memory-maintenance-operation-{0}.json" -f ([Guid]::NewGuid().ToString("n"))) + Write-JsonFile -Path $operationInputPath -Data $Operation + + try { + return Invoke-NodeJson -Arguments @( + $selfStateScriptPath, + "apply-operation", + "--canonical", $stateFilePath, + "--operation-file", $operationInputPath + ) + } finally { + Remove-Item -LiteralPath $operationInputPath -Force -ErrorAction SilentlyContinue + } +} + +function Convert-ToOperationArray { + param($Value) + + if ($null -eq $Value) { + return @() + } + + if ($Value -is [System.Array]) { + return @($Value) + } + + return @($Value) +} + +function Test-RecentLockPresent { + param([Parameter(Mandatory = $true)][string] $Path, [Parameter(Mandatory = $true)][int] $Minutes) + + if (-not (Test-Path $Path)) { + return $false + } + + $lockAge = [DateTime]::UtcNow - (Get-Item $Path).LastWriteTimeUtc + return $lockAge.TotalMinutes -lt $Minutes +} + +function Assert-AllowedMemoryMaintenanceOperation { + param($Operation) + + $operationName = Get-ObjectPropertyString -Value $Operation -Name "operation" + $allowed = @( + "append_distilled_memory", + "merge_incubation_support", + "queue_candidate_intervention", + "retire_candidate_intervention", + "propose_memory_distillation", + "apply_memory_distillation" + ) + + if ($null -eq $operationName -or -not $allowed.Contains($operationName)) { + throw "Memory maintenance proposed disallowed operation '$operationName'." + } +} + +trap { + $finishedAtUtc = [DateTime]::UtcNow + $failureMessage = $_.Exception.Message + + Write-JsonFile -Path $statusPath -Data @{ + status = "failed" + startedAt = $startedAtUtc.ToString("o") + finishedAt = $finishedAtUtc.ToString("o") + durationSeconds = [Math]::Round(($finishedAtUtc - $startedAtUtc).TotalSeconds, 2) + failureMessage = $failureMessage + skipModel = [bool]$SkipModel + stateFile = $stateFilePath + contextPath = $contextPath + operationOutputPath = $operationOutputPath + } + + Remove-Item -Path $lockPath -Force -ErrorAction SilentlyContinue + throw +} + +New-Item -ItemType Directory -Force -Path $statusDir, $logDir, (Split-Path -Parent $stateFilePath) | Out-Null + +if (Test-RecentLockPresent -Path $moderationLockPath -Minutes 20) { + Write-JsonFile -Path $statusPath -Data @{ + status = "skipped" + reason = "moderation_loop_active" + observedAt = ([DateTime]::UtcNow.ToString("o")) + stateFile = $stateFilePath + } + return +} + +if (Test-Path $lockPath) { + if (Test-RecentLockPresent -Path $lockPath -Minutes 20) { + Write-JsonFile -Path $statusPath -Data @{ + status = "skipped" + reason = "lock_present" + observedAt = ([DateTime]::UtcNow.ToString("o")) + stateFile = $stateFilePath + } + return + } + Remove-Item -LiteralPath $lockPath -Force +} + +Write-JsonFile -Path $lockPath -Data @{ + pid = $PID + startedAt = $startedAtUtc.ToString("o") +} + +if (Test-Path $operationOutputPath) { + Remove-Item -LiteralPath $operationOutputPath -Force +} + +foreach ($requiredPath in @($contextProjectionScriptPath, $selfStateScriptPath, $promptTemplatePath)) { + if (-not (Test-Path $requiredPath)) { + throw "Missing required helper at $requiredPath" + } +} + +$envValues = Read-DotEnv -Path (Join-Path $repoRoot ".env") +$codexExecutable = if ($envValues.ContainsKey("CODEX_EXECUTABLE") -and -not [string]::IsNullOrWhiteSpace($envValues["CODEX_EXECUTABLE"])) { $envValues["CODEX_EXECUTABLE"] } else { "codex" } +$codexModel = if ($envValues.ContainsKey("CODEX_MODEL") -and -not [string]::IsNullOrWhiteSpace($envValues["CODEX_MODEL"])) { $envValues["CODEX_MODEL"] } else { "gpt-5.4" } +$codexReasoningEffort = if ($envValues.ContainsKey("CODEX_MODEL_REASONING_EFFORT") -and -not [string]::IsNullOrWhiteSpace($envValues["CODEX_MODEL_REASONING_EFFORT"])) { $envValues["CODEX_MODEL_REASONING_EFFORT"] } else { "medium" } +$codexExecArgs = if ($envValues.ContainsKey("CODEX_EXEC_ARGS")) { Split-CommandArgs -Value $envValues["CODEX_EXEC_ARGS"] } else { @() } + +$typedContext = Get-TypedSelfState +$typedState = $typedContext.typedState + +Write-JsonFile -Path $contextPath -Data @{ + generated = "now" + stateFile = $stateFilePath + selfStateSummary = $typedContext.summary + mode = if ($typedState.scheduledRuntime.sleepCycle.isNapping) { "sleep_maintenance" } else { "awake_memory_maintenance" } + maintenanceBoundary = "This pass may propose only typed memory/incubation/candidate operations. Exact timestamps stay in typed state; prompt-facing chronology uses relative phrases." + memories = @(Project-MemoriesForRumination -Memories $typedState.thoughtMemory.memories -Now $startedAtUtc) + incubation = @(Project-IncubationForRumination -Threads $typedState.thoughtMemory.incubation -Now $startedAtUtc) + candidateInterventions = @(Project-InterventionsForRumination -Interventions $typedState.candidateInterventions.interventions -Now $startedAtUtc) + scheduledRuntime = Project-ScheduledRuntimeForRumination -Runtime $typedState.scheduledRuntime -Now $startedAtUtc + speechReceipts = @(Project-SpeechReceiptsForRumination -Receipts $typedState.speechReceipts.recentReceipts -Now $startedAtUtc) +} + +$prompt = Read-TextFile -Path $promptTemplatePath +$prompt = $prompt.Replace("{{CONTEXT_PATH}}", $contextPath) +$prompt = $prompt.Replace("{{STATE_FILE_PATH}}", $stateFilePath) +$prompt = $prompt.Replace("{{OPERATION_OUTPUT_PATH}}", $operationOutputPath) + +$exitCode = 0 +$combinedText = "" + +Write-JsonFile -Path $statusPath -Data @{ + status = if ($SkipModel) { "running_skip_model" } else { "running" } + startedAt = $startedAtUtc.ToString("o") + skipModel = [bool]$SkipModel + stateFile = $stateFilePath + contextPath = $contextPath + operationOutputPath = $operationOutputPath + codexExecutable = $codexExecutable + codexModel = $codexModel +} + +if ($SkipModel) { + Append-RunLog "skip-model: writing empty memory-maintenance proposal." + Write-JsonFile -Path $operationOutputPath -Data @() + $combinedText = "Skipped model memory maintenance by explicit -SkipModel smoke mode." +} else { + $codexArgs = @() + $codexArgs += $codexExecArgs + $codexArgs += @( + "exec", + "--ignore-user-config", + "-m", $codexModel, + "-c", 'approval_policy="never"', + "-c", ("model_reasoning_effort={0}" -f (ConvertTo-Json $codexReasoningEffort -Compress)), + "--dangerously-bypass-approvals-and-sandbox", + "--skip-git-repo-check", + "--json", + "-o", $lastMessagePath, + "-" + ) + + $execution = Invoke-CodexExec -Executable $codexExecutable -Arguments $codexArgs -WorkingDirectory $repoRoot -InputText $prompt + $exitCode = $execution.ExitCode + $combinedText = (($execution.StdOut, $execution.StdErr) | Where-Object { -not [string]::IsNullOrWhiteSpace($_) }) -join [Environment]::NewLine +} + +if ($exitCode -ne 0) { + throw $(if ([string]::IsNullOrWhiteSpace($combinedText)) { "Codex memory maintenance failed with exit code $exitCode." } else { $combinedText }) +} + +if (-not (Test-Path $operationOutputPath)) { + throw "Memory maintenance completed without writing operation output at $operationOutputPath." +} + +$proposedOperations = @(Convert-ToOperationArray -Value (Read-JsonFile -Path $operationOutputPath)) +$appliedOperations = @() + +foreach ($operation in $proposedOperations) { + Assert-AllowedMemoryMaintenanceOperation -Operation $operation + $appliedOperations += Apply-TypedOperation -Operation $operation +} + +$finishedAtUtc = [DateTime]::UtcNow +$lastMessage = if (Test-Path $lastMessagePath) { Get-Content -Path $lastMessagePath -Raw -Encoding UTF8 } else { "" } + +Write-JsonFile -Path $statusPath -Data @{ + status = "ok" + mode = if ($SkipModel) { "typed_memory_maintenance_skip_model" } else { "typed_memory_maintenance" } + startedAt = $startedAtUtc.ToString("o") + finishedAt = $finishedAtUtc.ToString("o") + durationSeconds = [Math]::Round(($finishedAtUtc - $startedAtUtc).TotalSeconds, 2) + exitCode = [int]$exitCode + skipModel = [bool]$SkipModel + stateFile = [string]$stateFilePath + contextPath = [string]$contextPath + operationOutputPath = [string]$operationOutputPath + proposedOperationCount = [int]@($proposedOperations).Count + appliedOperationCount = [int]@($appliedOperations).Count + stateUpdated = [bool](@($appliedOperations).Count -gt 0) + lastMessagePath = [string]$lastMessagePath +} + +Append-RunLog ("mode=typed_memory_maintenance proposed={0} applied={1}" -f @($proposedOperations).Count, @($appliedOperations).Count) +Remove-Item -Path $lockPath -Force -ErrorAction SilentlyContinue diff --git a/state/evidence.jsonl b/state/evidence.jsonl index 29264a7..e1b7ea8 100644 --- a/state/evidence.jsonl +++ b/state/evidence.jsonl @@ -109,3 +109,4 @@ {"ts":"2026-05-17T07:36:19Z","type":"implementation","status":"ok","note":"Reduced template-language leakage in Void's active memory surfaces: the typed self-state projection now renders memories and speaking pressure as natural-language thought surfaces instead of claim=/tension=/next= or needToSpeak labels, and the rumination prompt now frames the schema as a storage contract rather than Void's voice. Also removed obvious stock phrase generators from old memory-organ helpers."} {"ts":"2026-05-17T07:51:34Z","type":"implementation","status":"ok","note":"Moderation rumination now projects prompt-facing chronology through scripts/lib/void-rumination-context-projection.ps1: recent Discord, repo activity, receipts, memory, cursor, and runtime times render as relative phrases while exact timestamps stay in typed state/status/parent bookkeeping."} {"ts":"2026-05-17T08:01:06Z","type":"implementation","status":"ok","note":"Deleted the old memory-organ script family, legacy moderation-state template, legacy moderation context exporter, and legacy moderation-state store wrapper. Active code no longer references moderation-agent-state JSON/MessagePack or moderation_runtime; typed rumination and mood smokes pass, and runner operation application now uses operation files instead of inline PowerShell JSON."} +{"ts":"2026-05-17T08:43:39Z","type":"implementation","status":"ok","note":"Added the first typed memory-maintenance replacement runner. scripts/run-void-memory-maintenance.ps1 builds a bounded typed-state context, loads prompts/void-memory-maintenance.md, permits only memory/incubation/candidate operations, and applies them through scripts/void-self-state.mjs using operation files. Build and -SkipModel smoke pass; context has no ISO timestamp leaks. Next proof is a real model-pass fixture for meaning-preserving distillation."} diff --git a/state/map.yaml b/state/map.yaml index a5d39e2..c766464 100644 --- a/state/map.yaml +++ b/state/map.yaml @@ -133,6 +133,7 @@ landed_baseline: - The old `Void Moderator Rumination` and `Void Mood Drift` scheduled tasks are disabled. Their replacement entry scripts are typed-only: moderation now builds a bounded context packet with prompt-facing relative chronology, runs the reviewed rumination prompt from `prompts/void-moderator-rumination.md`, and applies typed operation payloads; mood updates typed sleep/speaking pressure in `.voidbot/private/void-self-state.cc`. - The active self-state service no longer registers, reads, or writes the legacy moderation document during routine operations. - The old memory-organ cleanup/vector-incubation/agency stack, legacy state template, legacy moderation context exporter, legacy moderation-state store, and JSON projection paths are deleted or offline; replacements must be typed-operation native. + - Memory maintenance now has a typed runner boundary: `scripts/run-void-memory-maintenance.ps1` builds prompt-facing typed context, loads `prompts/void-memory-maintenance.md`, rejects non-maintenance operations, and applies allowed memory/incubation/candidate proposals through `scripts/void-self-state.mjs`. - 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 evidence 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. @@ -151,5 +152,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 scheduled tasks are disabled; the moderation replacement has real rumination again through a reviewed prompt file plus typed operation output. - next_action: Harden the scheduled moderation phase machine and rebuild the memory-maintenance organ from the typed `.cc` state boundary. The next brain should derive cleanup, agency, sleep, incubation, and speech candidates from validated typed operations and model-owned decisions, not from legacy projection fields, mirror repair, or hard-wired compensator stacks. + 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. + next_action: Exercise the typed memory-maintenance runner with model-pass fixtures before scheduling it: prove a concrete memory such as `AquariumSynthCSharp: Workflow cannot own the body` survives distillation with subject, claim/question, evidence, tension, and action implication intact.