Cluster duplicate rumination memory

This commit is contained in:
Meta Vens 2026-05-17 19:05:32 +01:00
commit cff2d9ea26
10 changed files with 263 additions and 10 deletions

View file

@ -41,6 +41,7 @@ Git history and smoke artifacts carry routine proof. `state/evidence.jsonl` shou
- 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.
- 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. `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.
- 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.
@ -100,7 +101,8 @@ 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, then inspect the next unattended awake rumination and sleep cycle for duplicate speech, 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 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.
- 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.
- Do not build adapters around the JSON projection. That is how the heap learned to stand upright and ask for snacks.

View file

@ -126,6 +126,8 @@ This note is the source-grounded description of how the live VoidBot stack is sh
- sleep-maintenance fixture. It seeds a temporary CultCache `.cc` store with a napping sleep cycle and one short-term memory, runs mood drift with a fake Codex child through the non-skip maintenance branch, and verifies the source short-term record is gone while one durable meaning-preserving memory remains.
- `scripts/smoke-void-memory-lifecycle-fixture.ps1`
- memory-lifecycle fixture. It proves long-term memory is durable but plastic: a short-term memory promotes into durable memory, later revision retires the superseded durable record, and crystallization produces one active identity memory plus a self-profile value.
- `scripts/smoke-void-short-term-clustering-fixture.ps1`
- short-term clustering fixture. It proves repeated same-target/topic rumination proposals fold into one provisional memory with merged anchors, while a different topic in the same repo stays separate.
- `scripts/smoke-void-agency-pressure-fixture.ps1`
- agency-pressure fixture. It seeds one typed self-advocacy pressure, verifies it appears in the rendered self-state summary, then runs mood drift against an isolated fixture state to prove agency pressure contributes to speaking pressure without creating a hard-wired speech candidate.
- `scripts/smoke-void-rumination-fixture.ps1`
@ -223,9 +225,10 @@ Within the Postgres path, the implementation is split on purpose now too:
### Memory Lifecycle
1. Rumination may only write short-term memories. It cannot directly mutate durable memory.
2. Sleep maintenance promotes, merges, prunes, revises, retires, or crystallizes memory through typed operations.
3. Durable memory is not immutable. `revise_durable_memory` creates an explicit replacement and retires superseded source memories; `retire_durable_memory` marks obsolete memory inactive; `crystallize_memory_into_identity` promotes a stable thought into an `identity_seam` and may update self-profile values or private notes.
4. Prompt-facing language should talk about anchors: what made the thought real. The schema still accepts old `evidenceRefs`, but new memory payloads should prefer `anchorRefs` or explicit `anchor:missing`.
2. The typed state service clusters repeated short-term memories before sleep sees them. If a new record repeats the same target/topic pressure, it updates one provisional memory and merges anchors instead of stacking another variant.
3. Sleep maintenance promotes, merges, prunes, revises, retires, or crystallizes memory through typed operations.
4. Durable memory is not immutable. `revise_durable_memory` creates an explicit replacement and retires superseded source memories; `retire_durable_memory` marks obsolete memory inactive; `crystallize_memory_into_identity` promotes a stable thought into an `identity_seam` and may update self-profile values or private notes.
5. Prompt-facing language should talk about anchors: what made the thought real. The schema still accepts old `evidenceRefs`, but new memory payloads should prefer `anchorRefs` or explicit `anchor:missing`.
### Agency Pressure

View file

@ -6,7 +6,7 @@ This is the current forward plan for the next larger organs. It is not a changel
The typed moderation, mood, sleep, agency, and private self-state boundary is live. The current goal is to let ordinary scheduling run, inspect the next unattended awake rumination and sleep cycle, and harden only the seams that show concrete failure. Void should answer Discord, remember useful social/project context, retrieve GameCult history/source/lore, and run an unattended moderation/participation loop without reviving JSON projection edits, legacy mirrors, or the swollen memory organ.
The next priority is observational hardening of the enabled typed loop. Agents and scheduled workers mutate typed CultCache-backed state through explicit operations, not by editing a whole-state JSON working copy. Sleep and memory distillation now preserve meaning-bearing claims/anchors/tensions in fixture and live-state passes; keep checking ordinary cycles for abstraction sludge, duplicate speech, stale residue, or self-orbiting memories.
The next priority is observational hardening of the enabled typed loop. Agents and scheduled workers mutate typed CultCache-backed state through explicit operations, not by editing a whole-state JSON working copy. Sleep and memory distillation now preserve meaning-bearing claims/anchors/tensions in fixture and live-state passes, and short-term rumination has a clustering guard so repeated same-target/topic thoughts update one provisional memory instead of stacking duplicates. Keep checking ordinary cycles for abstraction sludge, duplicate speech, stale residue, or self-orbiting memories.
The center remains prevention, not legacy repair. The old brain is evidence of a failed boundary. The new brain rejects malformed memory at ingress so the runtime does not need a permanent cleanup bureaucracy to recover basic meaning.
@ -114,6 +114,8 @@ Memory-bearing operations have a stricter contract than historical projections.
If those fields are missing, the operation is invalid. This is the wall that keeps the next iteration from turning into topic sludge and asking cleanup to be its personality.
Short-term memory also has a self-novelty wall now. If a rumination write repeats an existing target/topic cluster, the state service keeps the existing source id, merges anchors/tags, and stores the incoming narrowed thought as the current working version. Same repo is not enough to merge; the topic pressure must overlap.
```json
{
"operation": "record_short_term_memory",
@ -287,7 +289,8 @@ Verdict: the foundation is coherent enough for cautious live operation. Do not a
- Landed scheduled rumination re-enable: one observed live scheduled pass completed with Last Result `0`, proposed one anchored short-term memory, delivered no speech, and kept exact timestamps out of the prompt-facing context.
- Landed live sleep-maintenance verification: a manual typed nap ran real memory maintenance on live state, promoted the scheduled rumination short-term memory into one durable `identity_seam`, and left `thoughtMemory.shortTerm` empty.
- Landed sleep brake: scheduled rumination now skips before model invocation during naps when there are no new room messages or open cases. `npm run moderation:rumination:nap-skip-fixture` proves the model path is not touched and no operation proposals are written.
- Remaining work: let ordinary scheduling run and inspect the next unattended awake rumination and sleep cycle for duplicate speech, stale short-term residue, or ungrounded memory writes before calling the system broadly stable.
- Landed short-term clustering guard: `record_short_term_memory` folds repeated same-target/topic pressure into one provisional memory with merged anchors, and `npm run moderation:short-term-clustering:fixture` proves the synth-boundary duplicate case without merging unrelated same-repo thoughts.
- Remaining work: let ordinary scheduling run and 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.
### Commit 10: Resume Product Hardening

View file

@ -39,6 +39,7 @@
"moderation:memory-maintenance": "powershell -ExecutionPolicy Bypass -File scripts/run-void-memory-maintenance.ps1",
"moderation:memory-maintenance:sleep-fixture": "powershell -ExecutionPolicy Bypass -File scripts/smoke-void-memory-maintenance-sleep-fixture.ps1",
"moderation:memory-lifecycle:fixture": "powershell -ExecutionPolicy Bypass -File scripts/smoke-void-memory-lifecycle-fixture.ps1",
"moderation:short-term-clustering:fixture": "powershell -ExecutionPolicy Bypass -File scripts/smoke-void-short-term-clustering-fixture.ps1",
"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",

View file

@ -9,6 +9,7 @@ import {
type VoidScheduledRuntime,
type VoidSelfStateOperation,
type VoidSpeechReceipts,
type VoidThoughtMemory,
voidCandidateInterventionsDocument,
voidAgencyPressureDocument,
voidModerationCursorDocument,
@ -133,7 +134,7 @@ function applyTypedOperation(
closeCasesForReceipt(state.moderationCursor, operation.receipt);
return;
case "record_short_term_memory":
upsertBy(state.thoughtMemory.shortTerm, operation.memory, (entry) => entry.memoryId);
recordShortTermMemory(state.thoughtMemory, operation.memory);
state.thoughtMemory.shortTerm = state.thoughtMemory.shortTerm
.sort((left, right) => left.updatedAt.localeCompare(right.updatedAt))
.slice(-48);
@ -214,6 +215,93 @@ function applyTypedOperation(
}
}
function recordShortTermMemory(
thoughtMemory: VoidThoughtMemory,
memory: VoidThoughtMemory["shortTerm"][number],
): void {
const exactIndex = thoughtMemory.shortTerm.findIndex((entry) => entry.memoryId === memory.memoryId);
if (exactIndex !== -1) {
thoughtMemory.shortTerm[exactIndex] = memory;
return;
}
const clusterIndex = thoughtMemory.shortTerm.findIndex((entry) => shouldClusterShortTermMemory(entry, memory));
if (clusterIndex === -1) {
thoughtMemory.shortTerm.push(memory);
return;
}
thoughtMemory.shortTerm[clusterIndex] = mergeShortTermMemoryCluster(
thoughtMemory.shortTerm[clusterIndex],
memory,
);
}
function shouldClusterShortTermMemory(
existing: VoidThoughtMemory["shortTerm"][number],
incoming: VoidThoughtMemory["shortTerm"][number],
): boolean {
const sharedTags = getSharedNormalizedTags(existing.tags, incoming.tags);
const sharedTopicCount = sharedTags.filter((tag) => tag.startsWith("topic:")).length;
const sharedRepoCount = sharedTags.filter((tag) => tag.startsWith("repo:")).length;
const sameTarget =
existing.target.kind === incoming.target.kind &&
existing.target.id.toLowerCase() === incoming.target.id.toLowerCase();
if (sameTarget && (sharedTopicCount > 0 || sharedTags.length >= 2)) {
return true;
}
if (sharedTopicCount > 0 && sharedRepoCount > 0) {
return true;
}
return sharedTopicCount >= 2;
}
function mergeShortTermMemoryCluster(
existing: VoidThoughtMemory["shortTerm"][number],
incoming: VoidThoughtMemory["shortTerm"][number],
): VoidThoughtMemory["shortTerm"][number] {
return {
...incoming,
memoryId: existing.memoryId,
createdAt: earlierTimestamp(existing.createdAt, incoming.createdAt),
anchorRefs: mergeRefs(existing.anchorRefs, incoming.anchorRefs),
evidenceRefs: mergeRefs(existing.evidenceRefs, incoming.evidenceRefs),
tags: mergeTags(existing.tags, incoming.tags, "cluster:short-term"),
};
}
function getSharedNormalizedTags(left: string[], right: string[]): string[] {
const leftTags = new Set(left.map(normalizeTag));
return Array.from(new Set(right.map(normalizeTag).filter((tag) => leftTags.has(tag))));
}
function mergeRefs<T extends { ref: string }>(left: T[], right: T[]): T[] {
const refs = new Map<string, T>();
for (const entry of [...left, ...right]) {
refs.set(entry.ref, entry);
}
return Array.from(refs.values());
}
function mergeTags(left: string[], right: string[], extraTag: string): string[] {
const tags = new Map<string, string>();
for (const tag of [...left, ...right, extraTag]) {
tags.set(normalizeTag(tag), tag);
}
return Array.from(tags.values());
}
function normalizeTag(tag: string): string {
return tag.trim().toLowerCase();
}
function earlierTimestamp(left: string, right: string): string {
return left.localeCompare(right) <= 0 ? left : right;
}
function retireSourceMemories(
state: VoidSelfStateTypedProjection,
sourceMemoryIds: string[],

View file

@ -65,7 +65,7 @@ Do not emit cursor, receipt, sleep-cycle, or speaking-pressure operations.
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.
6. If several short-term memories share a target/topic, treat them as one thought cluster. Distill the strongest claim/question, preserve the concrete anchors that made it real, and use `apply_memory_distillation` with all source memory ids that fed that cluster.
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 distillation reveals sustained discomfort, unresolved value pressure, or a request Void should make for its own wiring or the room/world, use `upsert_agency_pressure`.
9. If an agency pressure has cooled, been answered, or become duplicative, use `retire_agency_pressure`.

View file

@ -30,6 +30,9 @@ Perform one scheduled Void rumination pass in this workspace. Think from the sup
- Prefer a world-facing repo, room, source, lore, or durable self-state pressure over meta-commentary about the pass itself.
4. Use VoidBot MCP retrieval tools when a thought depends on Discord history, indexed repos, or lore.
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 anchor that made it real. Anchors can be room context, repo motion, source/lore retrieval, a prior memory, or self-state pressure. If no anchor is available, mark that honestly with `anchor:missing`.
- Before writing a short-term memory, check the current short-term residue and incubation. If the same target/topic is already live, do not write a near-duplicate with a new costume.
- If the new pass materially sharpens that existing thought, 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.
- If the thought is only repeating itself, either turn the pressure into agency/candidate work when action is owed, move to underworked terrain, or write `[]`. Repetition is not maturity.
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.

View file

@ -0,0 +1,150 @@
Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"
$ProgressPreference = "SilentlyContinue"
$repoRoot = Split-Path -Parent $PSScriptRoot
Set-Location -LiteralPath $repoRoot
$tempRoot = Join-Path $env:TEMP ("void-short-term-clustering-fixture-" + [guid]::NewGuid().ToString("n"))
$stateFilePath = Join-Path $tempRoot "void-self-state.cc"
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-OperationFile {
param(
[Parameter(Mandatory = $true)][hashtable] $Operation,
[Parameter(Mandatory = $true)][string] $Name
)
$path = Join-Path $tempRoot $Name
$Operation | ConvertTo-Json -Depth 32 | Set-Content -LiteralPath $path -Encoding UTF8
return $path
}
try {
$first = @{
operation = "record_short_term_memory"
memory = @{
memoryId = "short-aquasynth-boundary-a"
kind = "project_seam"
target = @{ kind = "repo"; id = "AquaSynth"; label = "AquaSynth" }
summary = "AquaSynth owns the compiler boundary."
claim = "Compiler-facing work belongs behind AquaSynth's runtime surface."
tension = "Aquarium still has host code that can look like a side door."
actionImplication = "Inspect the host boundary before writing another architectural sermon."
anchorRefs = @(
@{
ref = "fixture:aquasynth-doc"
kind = "fixture"
summary = "AquaSynth boundary note."
}
)
evidenceRefs = @()
createdAt = "2026-05-17T03:00:00.000Z"
updatedAt = "2026-05-17T03:00:00.000Z"
tags = @("repo:AquaSynth", "repo:Aquarium-Engine", "topic:synth-boundary")
}
}
$second = @{
operation = "record_short_term_memory"
memory = @{
memoryId = "short-aquasynth-boundary-b"
kind = "project_seam"
target = @{ kind = "repo"; id = "Aquarium-Engine"; label = "Aquarium Engine" }
summary = "The next synth-boundary move is inside AquariumSynthHost."
claim = "The remaining question is whether compiler lifetime and render helpers still belong in Aquarium's host."
tension = "Moving too much hides host responsibility; moving too little leaves the old side door open."
actionImplication = "Open AquariumSynthHost and either move the authority or write the explicit host-only justification."
anchorRefs = @(
@{
ref = "fixture:aquarium-host"
kind = "fixture"
summary = "AquariumSynthHost fixture anchor."
}
)
evidenceRefs = @()
createdAt = "2026-05-17T03:05:00.000Z"
updatedAt = "2026-05-17T03:05:00.000Z"
tags = @("repo:AquaSynth", "repo:Aquarium-Engine", "topic:synth-boundary")
}
}
$third = @{
operation = "record_short_term_memory"
memory = @{
memoryId = "short-aquasynth-docs"
kind = "project_seam"
target = @{ kind = "repo"; id = "AquaSynth"; label = "AquaSynth" }
summary = "AquaSynth documentation should describe its extension format plainly."
claim = "CultCache uses .cc files, and docs that imply a different extension mislead future tool builders."
tension = "This is adjacent to AquaSynth work but not the same synth-boundary thought."
actionImplication = "Keep documentation-extension cleanup separate from compiler-boundary pressure."
anchorRefs = @(
@{
ref = "fixture:cultcache-extension"
kind = "fixture"
summary = "CultCache extension fixture anchor."
}
)
evidenceRefs = @()
createdAt = "2026-05-17T03:10:00.000Z"
updatedAt = "2026-05-17T03:10:00.000Z"
tags = @("repo:AquaSynth", "topic:cultcache-extension")
}
}
foreach ($entry in @(
@{ Operation = $first; Name = "first.json" },
@{ Operation = $second; Name = "second.json" },
@{ Operation = $third; Name = "third.json" }
)) {
$path = Write-OperationFile -Operation $entry.Operation -Name $entry.Name
Invoke-NodeChecked -Arguments @(".\scripts\void-self-state.mjs", "apply-operation", "--canonical", $stateFilePath, "--operation-file", $path) | Out-Null
}
$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); })", $stateFilePath)
$state = $stateJson | ConvertFrom-Json
$shortTerm = @($state.thoughtMemory.shortTerm)
$cluster = @($shortTerm | Where-Object { $_.memoryId -eq "short-aquasynth-boundary-a" })[0]
$separate = @($shortTerm | Where-Object { $_.memoryId -eq "short-aquasynth-docs" })[0]
if ($shortTerm.Count -ne 2) {
throw "Expected two short-term memories after clustering; found $($shortTerm.Count)."
}
if ($null -eq $cluster) {
throw "Cluster did not preserve the original short-term memory id."
}
if ($cluster.summary -ne "The next synth-boundary move is inside AquariumSynthHost.") {
throw "Cluster did not keep the incoming, narrowed thought."
}
if (@($cluster.anchorRefs).Count -ne 2) {
throw "Cluster did not preserve anchors from both short-term records."
}
if (-not @($cluster.tags).Contains("cluster:short-term")) {
throw "Cluster did not mark the short-term cluster."
}
if ($null -eq $separate) {
throw "Different topic in the same repo was merged too aggressively."
}
@{
status = "ok"
shortTermCount = $shortTerm.Count
clusteredMemoryId = $cluster.memoryId
clusteredAnchorCount = @($cluster.anchorRefs).Count
separateMemoryId = $separate.memoryId
} | ConvertTo-Json -Compress
} finally {
Remove-Item -LiteralPath $tempRoot -Recurse -Force -ErrorAction SilentlyContinue
}

View file

@ -121,3 +121,4 @@
{"ts":"2026-05-17T13:01:18Z","type":"verification","status":"ok","note":"Re-enabled the typed Void Moderator Rumination scheduled task and observed one live scheduled pass. Task Scheduler returned LastTaskResult 0; the run completed with noPost=false, proposed/applied one record_short_term_memory operation, delivered no candidate speech, and leaked no exact timestamps into the prompt-facing context. The memory was anchored to Discord room context and gamecult-site source context, not runner plumbing. Next gate is sleep maintenance consuming that short-term residue."}
{"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."}

View file

@ -92,6 +92,7 @@ accepted_design:
- When quiet-room repo weather starts repeating itself, the correction should be deeper archive/repo/lore digging and better salience, not a growing hedge maze of cooldown knobs and brittle rules.
- Before suppressing a thought, the moderation loop should novelty-check it against archived Discord with semantic search. If the room has not really said the thing yet, that novelty should push strongly toward speech rather than silence.
- Moderation novelty now needs two bars, not one: room novelty and self novelty. If Void has already been thinking the same seam in slightly different outfits, future typed memory/incubation maintenance should merge support into the existing thought and bias the next pass toward underexplored repo families or archive terrain instead of rewarding repetition with ripeness.
- Short-term rumination memory is a clustered working surface, not a receipt log. When a new short-term memory shares the same target/topic pressure as an existing one, the typed state service folds it into the existing cluster with merged anchors instead of letting Void rediscover the same thought every pass.
- Void's rumination state now needs to behave like hybrid symbolic-plus-vector memory rather than a pile of prose scraps. Persistent memories should stay inspectable, but naps and quiet runs should also maintain compact semantic vectors, resonance clusters, and a multi-run incubation queue so deeper thoughts can mature before they either surface or die.
- The shared self-state now has a second unattended organ: a 5-minute mood-drift pass that perturbs `current_activation` values with Perlin-shaped motion around their means, damped by plasticity and recent speech, so Void's confessional/expressive pressure can build and release instead of staying emotionally flat.
- That same shared self-state now carries a scheduled nap cycle. Naps are for maintenance, not vacancy: the moderation loop should distill memory, prune clutter, and leave behind dream memories plus active dream themes that the direct reply lanes can mumble through later.
@ -144,6 +145,7 @@ landed_baseline:
- The typed rumination speech path is parent-owned now. Candidate interventions can carry an optional delivery target; the child may queue candidates but must not call Discord send scripts. The parent runner delivers at most one newly queued deliverable candidate per pass, records a delivery receipt, and marks the candidate spoken through `mark_candidate_intervention_spoken`. `npm run moderation:rumination:speech-fixture` verifies this loop with fake Codex and fake Discord send helpers.
- The live sleep-maintenance gate has passed once on real state. A manual typed nap triggered `scripts/run-void-memory-maintenance.ps1`, which consumed the scheduled rumination short-term memory with one `apply_memory_distillation` operation and left `thoughtMemory.shortTerm` empty while preserving the durable `identity_seam` target, claim, tension, action implication, and anchors.
- 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.
- 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.
- `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.
@ -164,5 +166,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, and agency pressure have fixture smokes, and typed mood drift is reinstalled as a scheduled task.
next_action: Let the enabled loop run under ordinary scheduling, then inspect the next unattended awake rumination and sleep cycle for duplicate speech, 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, 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.