Make sleep own durable memory promotion

This commit is contained in:
Meta Vens 2026-05-17 12:25:08 +01:00
commit 5df282e6b4
13 changed files with 130 additions and 44 deletions

View file

@ -633,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 owns the sleep transition. When Void is napping, it invokes `scripts/run-void-memory-maintenance.ps1` once per nap unless the pass already completed for that nap. Memory maintenance reads typed self-state, loads `prompts/void-memory-maintenance.md`, and applies only memory/incubation/candidate typed operation proposals. Use `-StateFilePath` for fixture stores, and `--skip-memory-maintenance` on `simulate-void-mood.mjs` for smoke runs that must not invoke Codex. Dream residue, identity crystallization, value pressure, and other agency behavior still have to come back through that operation boundary. No compatibility séance.
The mood-drift task owns the sleep transition. Rumination writes short-term memory only; when Void is napping, mood drift invokes `scripts/run-void-memory-maintenance.ps1` once per nap unless the pass already completed for that nap. Memory maintenance reads typed self-state, loads `prompts/void-memory-maintenance.md`, and applies only memory/incubation/candidate typed operation proposals. Short-term memories do not survive sleep unless promoted into durable memory or merged into incubation; the runner fails a real sleep pass that leaves short-term residue behind. Use `-StateFilePath` for fixture stores, and `--skip-memory-maintenance` on `simulate-void-mood.mjs` for smoke runs that must not invoke Codex. 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.

View file

@ -34,10 +34,11 @@ Git history and smoke artifacts carry routine proof. `state/evidence.jsonl` shou
- `scripts/simulate-void-mood.mjs` owns typed sleep/speaking-pressure over `.voidbot/private/void-self-state.cc`. During sleep, it invokes typed memory maintenance once per nap unless that nap already completed a pass; use `--skip-memory-maintenance` for smoke runs that must not call Codex.
- 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`. In sleep mode, memory pressure sets `sleepDirective.forceDistillation`; a real model pass returning no operations under pressure fails visibly.
- Rumination does not write durable memory. It can only emit `record_short_term_memory`; sleep maintenance must promote short-term records with `apply_memory_distillation`, merge them into incubation and prune them, or prune them outright. A real sleep pass that leaves short-term records behind fails.
- 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 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.
- Typed memory operations now enforce the first ingress wall for the next brain: rumination can only use `record_short_term_memory`, while sleep maintenance promotes with `apply_memory_distillation` or drops residue with `prune_short_term_memories`. Memory writes 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`.
- The Discord bot lane is no longer one swollen entrypoint; gateway wiring remains in `apps/bot/src/discord-bot.ts` while command/prompt handlers and Discord-shape support helpers now live under `apps/bot/src/discord-bot-*.ts`.

View file

@ -119,9 +119,9 @@ This note is the source-grounded description of how the live VoidBot stack is sh
- `scripts/lib/void-rumination-context-projection.ps1`
- 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.
- typed self-state operation CLI. It applies strict operation payloads such as cursor updates, open-case changes, delivery receipts, repo cursor updates, short-term memory records, sleep-owned distillations, 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.
- typed memory-maintenance runner. It builds a bounded prompt-facing context from typed short-term memories, durable 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`
@ -196,5 +196,5 @@ Within the Postgres path, the implementation is split on purpose now too:
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. In sleep mode, the context carries `sleepDirective.forceDistillation` when there is memory pressure. A real model pass that returns no operations under pressure fails visibly instead of letting yesterday's residue haunt the state.
7. In sleep mode, the context carries `sleepDirective.forceDistillation` when there is short-term memory pressure. A real model pass that returns no operations under pressure fails visibly, and any real sleep pass that leaves short-term memory behind fails instead of letting yesterday's residue haunt the state.
8. 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.

View file

@ -71,7 +71,7 @@ Runtime context dies at the end of the pass unless an explicit typed mutation pr
- `upsert_open_case`
- `close_open_case`
- `record_delivery_receipt`
- `append_distilled_memory`
- `record_short_term_memory`
- `merge_incubation_support`
- `queue_candidate_intervention`
- `retire_candidate_intervention`
@ -79,6 +79,7 @@ Runtime context dies at the end of the pass unless an explicit typed mutation pr
- `update_speaking_pressure`
- `propose_memory_distillation`
- `apply_memory_distillation`
- `prune_short_term_memories`
- Model/agent output crosses the boundary as proposed operations, not as rewritten state.
- The state service validates, normalizes, dedupes, and writes.
@ -96,7 +97,7 @@ The clean crossing surface is small JSON command payloads, not a whole JSON stat
}
```
Memory-bearing operations have a stricter contract than historical projections. A new distilled memory must carry:
Memory-bearing operations have a stricter contract than historical projections. A new rumination memory is short-term and must carry:
- a concrete target
- a summary
@ -109,12 +110,29 @@ If those fields are missing, the operation is invalid. This is the wall that kee
```json
{
"operation": "append_distilled_memory",
"kind": "project_seam",
"subject": "AquariumSynthCSharp",
"summary": "Workflow cannot own the body.",
"evidenceRefs": ["source:..."],
"observedAt": "2026-05-16T00:00:00.000Z"
"operation": "record_short_term_memory",
"memory": {
"memoryId": "mem-aquarium-body-workflow",
"kind": "project_seam",
"target": {
"kind": "repo",
"id": "AquariumSynthCSharp",
"label": "AquariumSynthCSharp"
},
"summary": "Workflow cannot own the body.",
"claim": "The implementation boundary should own runtime state instead of leaving workflow scripts to compensate for it.",
"tension": "Workflow scripts are good at orchestration and bad at being an organism.",
"actionImplication": "Move authority into the runtime boundary before adding more maintenance scripts.",
"evidenceRefs": [
{
"ref": "source:...",
"kind": "source"
}
],
"createdAt": "2026-05-16T00:00:00.000Z",
"updatedAt": "2026-05-16T00:00:00.000Z",
"tags": []
}
}
```

View file

@ -204,6 +204,7 @@ export const voidSpeechReceiptsSchema = z.object({
export const voidThoughtMemorySchema = z.object({
schemaVersion: z.literal(1),
shortTerm: z.array(distilledMemorySchema).default([]),
memories: z.array(distilledMemorySchema).default([]),
incubation: z.array(incubationThreadSchema).default([]),
updatedAt: timestampSchema,
@ -311,9 +312,15 @@ export const voidSelfStateOperationSchema = z.discriminatedUnion("operation", [
receipt: deliveryReceiptSchema,
}).strict(),
z.object({
operation: z.literal("append_distilled_memory"),
operation: z.literal("record_short_term_memory"),
memory: meaningPreservingMemorySchema,
}).strict(),
z.object({
operation: z.literal("prune_short_term_memories"),
sourceMemoryIds: z.array(nonEmptyStringSchema).min(1),
prunedAt: timestampSchema,
reason: z.string().trim().min(1).max(1000),
}).strict(),
z.object({
operation: z.literal("merge_incubation_support"),
thread: incubationThreadSchema,

View file

@ -70,6 +70,7 @@ export function createEmptyVoidSelfState(
}),
thoughtMemory: voidThoughtMemorySchema.parse({
schemaVersion: 1,
shortTerm: [],
memories: [],
incubation: [],
updatedAt: createdAt,
@ -119,6 +120,10 @@ export function renderVoidSelfStateSummary(
.slice(-6)
.reverse()
.map((memory) => renderTypedMemory(memory));
const shortTermMemories = state.thoughtMemory.shortTerm
.slice(-6)
.reverse()
.map((memory) => renderTypedMemory(memory));
const incubation = state.thoughtMemory.incubation
.filter((thread) => thread.status !== "retired")
.sort((left, right) => right.maturation - left.maturation)
@ -151,6 +156,9 @@ export function renderVoidSelfStateSummary(
memories.length > 0
? ["- What Void remembers:", ...memories].join("\n")
: "- What Void remembers: nothing durable yet.",
shortTermMemories.length > 0
? ["- Short-term residue awaiting sleep:", ...shortTermMemories].join("\n")
: "- Short-term residue awaiting sleep: none.",
incubation.length > 0
? ["- Thoughts still moving:", ...incubation].join("\n")
: "- Thoughts still moving: none.",

View file

@ -128,10 +128,19 @@ function applyTypedOperation(
state.speechReceipts.updatedAt = operation.receipt.sentAt;
closeCasesForReceipt(state.moderationCursor, operation.receipt);
return;
case "append_distilled_memory":
upsertBy(state.thoughtMemory.memories, operation.memory, (entry) => entry.memoryId);
case "record_short_term_memory":
upsertBy(state.thoughtMemory.shortTerm, operation.memory, (entry) => entry.memoryId);
state.thoughtMemory.shortTerm = state.thoughtMemory.shortTerm
.sort((left, right) => left.updatedAt.localeCompare(right.updatedAt))
.slice(-48);
state.thoughtMemory.updatedAt = operation.memory.updatedAt;
return;
case "prune_short_term_memories":
state.thoughtMemory.shortTerm = state.thoughtMemory.shortTerm.filter(
(entry) => !operation.sourceMemoryIds.includes(entry.memoryId),
);
state.thoughtMemory.updatedAt = operation.prunedAt;
return;
case "merge_incubation_support":
upsertBy(state.thoughtMemory.incubation, operation.thread, (entry) => entry.threadId);
state.thoughtMemory.updatedAt = operation.thread.updatedAt;
@ -158,6 +167,9 @@ function applyTypedOperation(
state.thoughtMemory.memories = state.thoughtMemory.memories.filter(
(entry) => !operation.sourceMemoryIds.includes(entry.memoryId) || entry.memoryId === operation.memory.memoryId,
);
state.thoughtMemory.shortTerm = state.thoughtMemory.shortTerm.filter(
(entry) => !operation.sourceMemoryIds.includes(entry.memoryId),
);
state.thoughtMemory.updatedAt = operation.appliedAt;
return;
}

View file

@ -22,7 +22,10 @@ operation payloads worth applying.
## Purpose
Sleep forces distillation. Memory maintenance is allowed to reduce bulk, not
Sleep forces distillation. The `shortTermMemories` list is the day's residue.
None of it survives sleep as short-term memory. Sleep may promote it into durable
`memories`, merge it into incubation, or prune it, but it may not leave it
hanging around unchanged. Memory maintenance is allowed to reduce bulk, not
meaning.
A valid memory/distillation operation must preserve:
@ -33,33 +36,35 @@ A valid memory/distillation operation must preserve:
- 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, retire stale candidates, or write
nothing only when the state is already minimal or no meaning-preserving operation
is possible.
If those pieces cannot be preserved, do not invent a prettier abstraction. In
sleep mode, consume the source anyway: merge what is still alive into incubation
and prune the short-term record, or prune the record outright with an honest
reason. Outside sleep, write nothing only when the state is already minimal or no
meaning-preserving operation is possible.
## Allowed Operations
- `append_distilled_memory`
- `merge_incubation_support`
- `queue_candidate_intervention`
- `retire_candidate_intervention`
- `propose_memory_distillation`
- `apply_memory_distillation`
- `prune_short_term_memories`
Do not emit cursor, receipt, sleep-cycle, or speaking-pressure operations.
## Pass Shape
1. Read the context file.
2. If `sleepDirective.forceDistillation` is true, make at least one meaning-preserving pruning, merge, distillation, or retirement move unless the context proves none is possible.
3. If there are no memories, no incubation threads, and no candidate interventions needing attention, write `[]`.
4. Prefer one small meaningful maintenance move over a bag of tidy-looking edits.
5. If two memories really collapse into one stronger memory, use `apply_memory_distillation`.
6. If a thought is still alive but undercooked, use `merge_incubation_support`.
7. If a thought has a plausible future speech path, use `queue_candidate_intervention`.
8. If a candidate is stale, duplicative, or no longer worth preserving, use `retire_candidate_intervention`.
9. Write `{{OPERATION_OUTPUT_PATH}}`. If nothing deserves persistence, write `[]`.
2. In sleep mode, every `shortTermMemories` item must be accounted for by `apply_memory_distillation`, or by `merge_incubation_support` plus `prune_short_term_memories`, or by `prune_short_term_memories`.
3. If `sleepDirective.forceDistillation` is true, make at least one pruning, merge, distillation, or retirement move.
4. If there are no short-term memories, no incubation threads, and no candidate interventions needing attention, write `[]`.
5. Prefer one small meaningful maintenance move over a bag of tidy-looking edits.
6. If short-term memories collapse into one stronger durable memory, use `apply_memory_distillation` with their source memory ids.
7. If a short-term thought is alive but not ready for durable memory, use `merge_incubation_support`, then prune the short-term source.
8. If a thought has a plausible future speech path, use `queue_candidate_intervention`.
9. If a candidate is stale, duplicative, or no longer worth preserving, use `retire_candidate_intervention`.
10. Write `{{OPERATION_OUTPUT_PATH}}`. Outside sleep, if nothing deserves persistence, write `[]`.
## Voice Discipline

View file

@ -1,6 +1,6 @@
# Void Moderator Rumination
Perform one scheduled Void rumination pass in this workspace. Think from the supplied state and evidence, then return only the durable changes worth keeping.
Perform one scheduled Void rumination pass in this workspace. Think from the supplied state and evidence, then return only typed operation payloads worth keeping until sleep judges them.
## Required Reading
@ -26,7 +26,7 @@ Perform one scheduled Void rumination pass in this workspace. Think from the sup
2. If Void owes the room an answer, work on that before private wandering.
3. If the room is quiet, follow one concrete pressure from memory, recent repo activity, archived Discord history, indexed source, or lore.
4. Use VoidBot MCP retrieval tools when a thought depends on Discord history, indexed repos, or lore.
5. A saved memory should sound like something Void actually noticed, not a form label. Keep the subject, the claim or question, the pressure against it, why it matters for the next move, and the evidence that made it real. If evidence is absent, mark that honestly with `evidence:missing`.
5. A new memory from rumination is short-term. Use `record_short_term_memory`, not durable memory operations. It should sound like something Void actually noticed, not a form label. Keep the subject, the claim or question, the pressure against it, why it matters for the next move, and the evidence that made it real. If evidence is absent, mark that honestly with `evidence:missing`.
6. Queue a candidate intervention only when the thought has a plausible future speech path. Draft it in Void's voice, not in maintenance language.
7. If speaking is warranted, use `scripts/send-discord-message.ps1` or `scripts/send-discord-message.mjs`. If `{{NO_POST}}` is true, do not send; queue a candidate instead.
8. Write `{{OPERATION_OUTPUT_PATH}}`. If nothing deserves persistence, write `[]`.
@ -35,12 +35,10 @@ Perform one scheduled Void rumination pass in this workspace. Think from the sup
- `upsert_open_case`
- `close_open_case`
- `append_distilled_memory`
- `record_short_term_memory`
- `merge_incubation_support`
- `queue_candidate_intervention`
- `retire_candidate_intervention`
- `propose_memory_distillation`
- `apply_memory_distillation`
## Parent-Owned Operations
@ -50,8 +48,11 @@ Do not emit these operations from rumination:
- `record_delivery_receipt`
- `update_sleep_cycle`
- `update_speaking_pressure`
- `propose_memory_distillation`
- `apply_memory_distillation`
- `prune_short_term_memories`
The parent runner owns cursor and receipt recording. Mood maintenance owns sleep and speaking-pressure updates.
The parent runner owns cursor and receipt recording. Mood maintenance owns sleep and speaking-pressure updates. Sleep memory maintenance owns promotion from short-term memory into durable memory.
## Voice Discipline

View file

@ -267,12 +267,12 @@ function Assert-AllowedMemoryMaintenanceOperation {
$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"
"apply_memory_distillation",
"prune_short_term_memories"
)
if ($null -eq $operationName -or -not $allowed.Contains($operationName)) {
@ -349,7 +349,8 @@ $codexExecArgs = if ($envValues.ContainsKey("CODEX_EXEC_ARGS")) { Split-CommandA
$typedContext = Get-TypedSelfState
$typedState = $typedContext.typedState
$isSleepMaintenance = [bool]$typedState.scheduledRuntime.sleepCycle.isNapping
$memoryCount = @(Convert-ToValueArray -Value $typedState.thoughtMemory.memories).Count
$shortTermMemoryCount = @(Convert-ToValueArray -Value $typedState.thoughtMemory.shortTerm).Count
$longTermMemoryCount = @(Convert-ToValueArray -Value $typedState.thoughtMemory.memories).Count
$incubationCount = @(Convert-ToValueArray -Value $typedState.thoughtMemory.incubation).Count
$activeCandidateCount = @(
@(Convert-ToValueArray -Value $typedState.candidateInterventions.interventions) |
@ -358,7 +359,7 @@ $activeCandidateCount = @(
$status -eq "queued" -or $status -eq "deferred"
}
).Count
$maintenancePressure = $memoryCount + $incubationCount + $activeCandidateCount
$maintenancePressure = $shortTermMemoryCount + $incubationCount + $activeCandidateCount
Write-JsonFile -Path $contextPath -Data @{
generated = "now"
@ -367,13 +368,15 @@ Write-JsonFile -Path $contextPath -Data @{
mode = if ($isSleepMaintenance) { "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."
sleepDirective = @{
forceDistillation = [bool]($isSleepMaintenance -and $maintenancePressure -gt 1)
forceDistillation = [bool]($isSleepMaintenance -and $maintenancePressure -gt 0)
maintenancePressure = [int]$maintenancePressure
memoryCount = [int]$memoryCount
shortTermMemoryCount = [int]$shortTermMemoryCount
longTermMemoryCount = [int]$longTermMemoryCount
incubationCount = [int]$incubationCount
activeCandidateCount = [int]$activeCandidateCount
rule = "During sleep, yesterday's rumination residue must be pruned or compressed. Return [] only when the typed surfaces are already minimal or no meaning-preserving operation is possible."
}
shortTermMemories = @(Project-MemoriesForRumination -Memories $typedState.thoughtMemory.shortTerm -Now $startedAtUtc)
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)
@ -436,7 +439,7 @@ if (-not (Test-Path $operationOutputPath)) {
$proposedOperations = @(Convert-ToOperationArray -Value (Read-JsonFile -Path $operationOutputPath))
$appliedOperations = @()
if ((-not $SkipModel) -and $isSleepMaintenance -and $maintenancePressure -gt 1 -and @($proposedOperations).Count -eq 0) {
if ((-not $SkipModel) -and $isSleepMaintenance -and $maintenancePressure -gt 0 -and @($proposedOperations).Count -eq 0) {
throw "Sleep memory maintenance returned no operations despite maintenance pressure $maintenancePressure."
}
@ -445,6 +448,14 @@ foreach ($operation in $proposedOperations) {
$appliedOperations += Apply-TypedOperation -Operation $operation
}
if ((-not $SkipModel) -and $isSleepMaintenance) {
$refreshedTypedState = Get-TypedSelfState
$remainingShortTermCount = @(Convert-ToValueArray -Value $refreshedTypedState.typedState.thoughtMemory.shortTerm).Count
if ($remainingShortTermCount -gt 0) {
throw "Sleep memory maintenance left $remainingShortTermCount short-term memories unpromoted."
}
}
$finishedAtUtc = [DateTime]::UtcNow
$lastMessage = if (Test-Path $lastMessagePath) { Get-Content -Path $lastMessagePath -Raw -Encoding UTF8 } else { "" }

View file

@ -277,6 +277,24 @@ function Convert-ToOperationArray {
return @($Value)
}
function Assert-AllowedRuminationOperation {
param($Operation)
$operationName = Get-ObjectPropertyString -Value $Operation -Name "operation"
$allowed = @(
"upsert_open_case",
"close_open_case",
"record_short_term_memory",
"merge_incubation_support",
"queue_candidate_intervention",
"retire_candidate_intervention"
)
if ($null -eq $operationName -or -not $allowed.Contains($operationName)) {
throw "Rumination proposed disallowed operation '$operationName'."
}
}
function Convert-LastSpeechToReceiptOperation {
param($Speech)
@ -440,6 +458,7 @@ Write-JsonFile -Path $contextPath -Data @{
openCases = @(Project-OpenCasesForRumination -Cases $typedState.moderationCursor.openCases -Now $startedAtUtc)
speechReceipts = @(Project-SpeechReceiptsForRumination -Receipts $typedState.speechReceipts.recentReceipts -Now $startedAtUtc)
memories = @(Project-MemoriesForRumination -Memories $typedState.thoughtMemory.memories -Now $startedAtUtc)
shortTermMemories = @(Project-MemoriesForRumination -Memories $typedState.thoughtMemory.shortTerm -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
@ -518,6 +537,7 @@ foreach ($operation in $proposedOperations) {
if ($null -eq $operation -or $null -eq $operationName) {
continue
}
Assert-AllowedRuminationOperation -Operation $operation
$appliedOperations += Apply-TypedOperation -Operation $operation
}

View file

@ -111,3 +111,5 @@
{"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."}
{"ts":"2026-05-17T11:09:32Z","type":"implementation","status":"ok","note":"Sleep now owns memory distillation. simulate-void-mood.mjs invokes typed memory maintenance once per nap, can skip it for smoke, and records the result in mood status. run-void-memory-maintenance.ps1 marks sleep-mode pressure with sleepDirective.forceDistillation and fails real sleep passes that return no operations while memory pressure exists. Fixture smoke confirmed napping context with two concrete AquariumSynthCSharp memories sets forceDistillation=true without leaking raw timestamps."}
{"ts":"2026-05-17T11:16:42Z","type":"implementation","status":"ok","note":"Made sleep distillation a state invariant instead of a prompt preference. Typed thought memory now has shortTerm plus durable memories; rumination may only emit record_short_term_memory, while durable promotion happens through sleep-owned apply_memory_distillation. Sleep maintenance now fails a real pass if short-term memories remain afterward. Fixture smoke verified record_short_term_memory creates only short-term state and apply_memory_distillation promotes it while clearing the source."}
{"ts":"2026-05-17T11:24:20Z","type":"implementation","status":"ok","note":"Closed the durable-memory side door: removed append_distilled_memory from the typed operation schema/service, kept rumination limited to record_short_term_memory, and verified direct durable append is rejected while sleep-owned apply_memory_distillation promotes and clears short-term sources."}

View file

@ -67,7 +67,7 @@ accepted_design:
- Bot, worker, providers, and RAG remain the main structural seams.
- State storage stays intentionally split: Postgres for operational state, Qdrant for vectors, `.voidbot/` for archives, artifacts, logs, and backup material.
- Private Void self-state lives behind one polymorphic CultCache-backed `.cc` authority with typed document kinds for self profile, moderation cursor/open obligations, speech receipts, thought memory/incubation, scheduled runtime, and candidate interventions. Do not mistake typed document ownership for separate physical stores.
- Agents and scheduled runners should cross the private-state boundary with small typed mutation operations such as cursor updates, open-case upserts, receipt records, memory appends, incubation merges, and candidate-intervention changes.
- Agents and scheduled runners should cross the private-state boundary with small typed mutation operations such as cursor updates, open-case upserts, receipt records, short-term memory records, sleep-owned distillations, incubation merges, and candidate-intervention changes.
- JSON may be used for operation payloads or debug exports, but not as an editable working projection of canonical self-state.
- Deterministic cleanup code should not become a permanent organ just because recent manual cleanup requests accidentally produced compensators. The desired architecture prevents bad state from entering, then uses narrow migrations for historical debris.
- `owner_codex` and `local_llm` remain distinct lanes with different tool-execution and safety posture.
@ -135,6 +135,7 @@ landed_baseline:
- 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`.
- Sleep is the forcing function for memory distillation. `scripts/simulate-void-mood.mjs` invokes typed memory maintenance once per nap, and a real sleep pass with memory pressure must return at least one operation instead of letting yesterday's rumination residue linger.
- Rumination writes short-term memory only through `record_short_term_memory`. Durable memory is sleep-owned: short-term residue must be promoted with `apply_memory_distillation`, merged into incubation and pruned, or pruned outright before sleep completes.
- 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.