From d2bf37f93cb1f3e04d0bd5243e29b3fc1c0978d1 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Sun, 17 May 2026 13:25:55 +0100 Subject: [PATCH] Add durable memory lifecycle operations --- README.md | 1 + notes/fresh-workspace-handoff.md | 11 +- notes/voidbot-current-system-map.md | 17 +- notes/voidbot-implementation-plan.md | 22 ++- package.json | 1 + packages/core/src/void-self-state-domain.ts | 58 +++++-- .../core/src/void-self-state-projection.ts | 5 +- packages/core/src/void-self-state-service.ts | 58 ++++++- prompts/void-memory-maintenance.md | 14 +- prompts/void-moderator-rumination.md | 6 +- .../void-rumination-context-projection.ps1 | 3 + scripts/run-void-memory-maintenance.ps1 | 3 + .../smoke-void-agency-pressure-fixture.ps1 | 3 +- .../smoke-void-memory-lifecycle-fixture.ps1 | 157 ++++++++++++++++++ ...-void-memory-maintenance-sleep-fixture.ps1 | 10 +- state/evidence.jsonl | 1 + state/map.yaml | 16 +- 17 files changed, 331 insertions(+), 55 deletions(-) create mode 100644 scripts/smoke-void-memory-lifecycle-fixture.ps1 diff --git a/README.md b/README.md index 55b056d..a927aa3 100644 --- a/README.md +++ b/README.md @@ -634,6 +634,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. 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, `--skip-memory-maintenance` on `simulate-void-mood.mjs` for smoke runs that must not invoke Codex, and `npm run moderation:memory-maintenance:sleep-fixture` to exercise the non-skip model branch with a fake Codex child. Dream residue, identity crystallization, value pressure, and other agency behavior still have to come back through that operation boundary. No compatibility séance. +Long-term memory is durable, not frozen. Sleep can promote short-term residue into durable memory, and later maintenance can explicitly revise, retire, or crystallize durable memories instead of silently overwriting Void's past thoughts. Use `npm run moderation:memory-lifecycle:fixture` to verify promotion, revision, retirement, and crystallization into an identity memory plus self-profile value. The live language is anchors, not courtroom evidence: a thought needs something that made it real, whether that is room context, repo motion, source/lore retrieval, a prior memory, or self-state pressure. Agency pressure is also typed state now, not hard-wired behavior. `upsert_agency_pressure` records sustained discomfort, active tension, self-advocacy requests, and world-advocacy requests as first-class pressure; candidate interventions may emerge from that pressure, but they are not the pressure itself. Use `npm run moderation:agency-pressure:fixture` to verify agency pressure enters the `.cc`, appears in the self-state summary, and contributes to mood speaking pressure. 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 52dc9dd..b18ee97 100644 --- a/notes/fresh-workspace-handoff.md +++ b/notes/fresh-workspace-handoff.md @@ -32,16 +32,17 @@ Git history and smoke artifacts carry routine proof. `state/evidence.jsonl` shou - `scripts/run-void-moderator-rumination.ps1` has been rebuilt as a typed rumination runner. It builds a typed context packet, loads `prompts/void-moderator-rumination.md`, runs Codex rumination, and applies only the JSON operation payloads that come back through `scripts/void-self-state.mjs`. Cursor and receipt bookkeeping remain parent-owned. - 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` 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. -- `npm run moderation:memory-maintenance:sleep-fixture` now proves the non-skip sleep path without spending a live model call: it seeds a temporary `.cc`, forces mood drift through sleep maintenance, uses a fake Codex child to propose `apply_memory_distillation`, and verifies short-term residue is gone while the durable memory keeps subject, claim, tension, action implication, and evidence. +- `npm run moderation:memory-maintenance:sleep-fixture` now proves the non-skip sleep path without spending a live model call: it seeds a temporary `.cc`, forces mood drift through sleep maintenance, uses a fake Codex child to propose `apply_memory_distillation`, and verifies short-term residue is gone while the durable memory keeps subject, claim, tension, action implication, and anchor. - The local scheduled task `Void Mood Drift` has been reinstalled/enabled against the typed runner and manually started once with Last Result `0`. `Void Moderator Rumination` remains disabled until its model-pass fixture earns unattended runtime. - 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. +- 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. - 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: 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. +- 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, anchors, live tension, and future-action implication survive compression. Bulk can die. Meaning does not get put through the office shredder and called a dream. +- 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 anchors unless the operation explicitly tags `anchor: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`. @@ -84,7 +85,7 @@ Git history and smoke artifacts carry routine proof. `state/evidence.jsonl` shou - That cleanup is now harsher and less sentimental. Sleep-side consolidation trims semantic memory harder, cuts dream memory down, throws away excess repo/archive/runtime residue, and explicitly keeps the freshest surviving records after sort instead of accidentally preserving the oldest fossils in the file. - `recent-preoccupation` and `quiet-room-status` are not durable thought kinds, person-singleton labels do not get to crystallize into doctrine, and boilerplate like `looks like a live seam` is template smell. - The old moderation-agent state and its top-level mirrors are not source material for the next brain. Treat them as failed-boundary evidence only. -- The replacement sleep/distillation path must stop "helping" already-good memories into abstraction sludge. A repo-bound memory like `AquariumSynthCSharp: Workflow cannot own the body` must survive with subject, claim, evidence, tension, and implication intact. +- The replacement sleep/distillation path must stop "helping" already-good memories into abstraction sludge. A repo-bound memory like `AquariumSynthCSharp: Workflow cannot own the body` must survive with subject, claim, anchor, tension, and implication intact. - Values and personality activations still need an autonomy path, but it must be rebuilt through typed operations and model-owned judgment rather than legacy runtime buckets. - The important distinction is outward too, not just inward. Self-directed tensions can ask for prompt/memory/runtime surgery, while world-directed tensions can turn repo/lore/room opinions into real requests or first-person interventions when the same objection survives more than one pass. - The intended shape now is: quiet runs can follow one seam for several passes, connect repo motion to lore or philosophy if the evidence supports it, and only surface with the juiciest version once the incubation queue says the thought has actually grown teeth. @@ -98,7 +99,7 @@ Git history and smoke artifacts carry routine proof. `state/evidence.jsonl` shou - Stop feature work on moderation, mood, agency, and public-lane behavior until the private-state boundary is rebuilt. - First bounded move after the current commit: harden the typed scheduled rumination phase machine, then rebuild memory maintenance/sleep as typed operations with the existing meaning-preservation gate. - 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. -- Rebuild sleep/distillation before trusting it again. Fixture check: a memory like `AquariumSynthCSharp: Workflow cannot own the body` must survive sleep with its concrete subject, claim, evidence, tension, and implication intact. +- Rebuild sleep/distillation before trusting it again. 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. ## Important Paths diff --git a/notes/voidbot-current-system-map.md b/notes/voidbot-current-system-map.md index a6413bf..a228538 100644 --- a/notes/voidbot-current-system-map.md +++ b/notes/voidbot-current-system-map.md @@ -119,11 +119,13 @@ 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, 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. + - 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, durable-memory revisions/retirements/crystallizations, incubation merges, agency pressure, 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 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/smoke-void-memory-maintenance-sleep-fixture.ps1` - 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-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/install-moderation-rumination-task.ps1` @@ -200,16 +202,23 @@ Within the Postgres path, the implementation is split on purpose now too: 1. `scripts/run-void-memory-maintenance.ps1` is invoked by mood drift during sleep, and can also be run manually for fixtures. 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. +3. It writes `.voidbot/status/void-memory-maintenance-context.json` with prompt-facing relative chronology and only typed memory/incubation/agency/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. +5. The parent runner rejects any operation outside memory lifecycle, incubation, agency pressure, 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 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. The fixture smoke exercises this path with a fake Codex child so the parent validation and application machinery are tested without relying on live inference. 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. +### 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`. + ### Agency Pressure 1. Rumination and memory maintenance may propose `upsert_agency_pressure` or `retire_agency_pressure`. -2. The typed service validates that each pressure has a concrete target, claim or question, action implication, intensity, and either evidence refs/source memory ids or an explicit `evidence:missing` tag. +2. The typed service validates that each pressure has a concrete target, claim or question, action implication, intensity, and either anchors/source memory ids or an explicit `anchor:missing` tag. 3. The self-state summary renders active agency pressure separately from queued speech candidates. 4. Mood drift reads active and ready agency pressure into speaking pressure, but it does not manufacture speech text from it. diff --git a/notes/voidbot-implementation-plan.md b/notes/voidbot-implementation-plan.md index 317ed85..38e11f8 100644 --- a/notes/voidbot-implementation-plan.md +++ b/notes/voidbot-implementation-plan.md @@ -6,7 +6,7 @@ This is the current forward plan for the next larger organs. It is not a changel Stop feature work on moderation, mood, and self-state until the state boundary is rebuilt. The live product goal still stands: Void should answer Discord, remember useful social/project context, retrieve GameCult history/source/lore, and run an unattended moderation/participation loop. The old implementation reached that goal through JSON projection edits, legacy mirrors, and a swollen memory organ; that path is now offline. Most of the deterministic cleanup code is recent compensator cruft: it accumulated because the live state had exploded into nonsense, and repeated "clean this file" passes preserved the mess by adding machinery around it. -The next priority is to rebuild the scheduled loop and memory maintenance on the typed CultCache `.cc` state boundary so the compensator pile is unnecessary. Agents and scheduled workers should mutate typed CultCache-backed state through explicit tools/APIs, not by editing a whole-state JSON working copy. Sleep and memory distillation must also be redesigned from first principles so they preserve meaning-bearing claims/evidence/tensions instead of collapsing them into generic slogan paste. +The next priority is to rebuild the scheduled loop and memory maintenance on the typed CultCache `.cc` state boundary so the compensator pile is unnecessary. Agents and scheduled workers should mutate typed CultCache-backed state through explicit tools/APIs, not by editing a whole-state JSON working copy. Sleep and memory distillation must also be redesigned from first principles so they preserve meaning-bearing claims/anchors/tensions instead of collapsing them into generic slogan paste. The center of the next pass is prevention, not legacy repair. The old brain can be treated as evidence of a failed boundary. The new brain should reject malformed memory at ingress so the runtime never needs a permanent cleanup bureaucracy to recover basic meaning. @@ -34,6 +34,7 @@ The center of the next pass is prevention, not legacy repair. The old brain can - `void.speech_receipts`: recent delivered replies and dedupe keys - `void.thought_memory`: distilled memories, incubation threads, resonance summaries - `void.scheduled_runtime`: sleep cycle, speaking pressure, last run summaries + - `void.agency_pressure`: discomforts, active tensions, and self/world advocacy pressure - `void.candidate_interventions`: drafts or requests that may become speech CultCache does not need a separate physical store per state type; it is already polymorphic. The intended split is ownership by typed document kind and mutation contract, not a herd of little stores in fake mustaches. Each durable fact still has one owner. No top-level mirrors. No editable shadow copy pretending to be a harmless view. @@ -52,7 +53,7 @@ CultCache does not need a separate physical store per state type; it is already - current cursor window - outstanding obligations - selected memory/thought summaries - - selected repo/archive evidence + - selected repo/archive anchors - proposed state mutations - optional speech request - Mood drift runtime is deterministic maintenance: @@ -81,6 +82,9 @@ Runtime context dies at the end of the pass unless an explicit typed mutation pr - `update_speaking_pressure` - `propose_memory_distillation` - `apply_memory_distillation` + - `revise_durable_memory` + - `retire_durable_memory` + - `crystallize_memory_into_identity` - `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. @@ -106,7 +110,7 @@ Memory-bearing operations have a stricter contract than historical projections. - at least one claim or question - a tension or counterweight - an action implication -- evidence refs, or an explicit `evidence:missing` tag +- anchors, or an explicit `anchor:missing` tag 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. @@ -125,7 +129,7 @@ If those fields are missing, the operation is invalid. This is the wall that kee "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": [ + "anchorRefs": [ { "ref": "source:...", "kind": "source" @@ -146,7 +150,7 @@ The runner can hand these to a CLI/MCP tool. The store decides what survives. - Recent room context is derived from Discord archive and the current poll. - Prompt-facing recent chronology is derived as relative phrases. Exact timestamps remain state/status/cursor data and should not be fed to the child rumination loop unless a tool or parent-owned operation specifically needs them. - Topic saturation is derived from incubation/memory support, not separately stored as another truth. -- Source coverage is derived from recent evidence refs and repo/archive metadata. +- Source coverage is derived from recent anchors and repo/archive metadata. - Need-to-speak can keep a small pressure field, but its inputs remain receipts, candidates, and time since last speech. - Legacy top-level mirrors are deleted. If a consumer needs a projection, render it on demand. @@ -244,7 +248,7 @@ Verdict: stop feature work and rebuild this foundation. The rest of the machine - Continue hardening the typed rumination runner's explicit phases: - poll chronology - read typed state summary - - collect evidence from Discord/source/lore only when a phase asks for it + - collect anchors from Discord/source/lore only when a phase asks for it - ask model for decisions and operation payloads - validate/apply operations - optionally send speech @@ -264,12 +268,14 @@ Verdict: stop feature work and rebuild this foundation. The rest of the machine - 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`. - Landed sleep integration: `scripts/simulate-void-mood.mjs` invokes memory maintenance once per nap, and the maintenance runner fails a real sleep pass that returns no operations while memory pressure is present. -- Landed verification: `npm run moderation:memory-maintenance:sleep-fixture` exercises the non-skip model branch with a fake Codex child, proving `AquariumSynthCSharp: Workflow cannot own the body` promotes from short-term into durable memory while preserving subject, claim, evidence, tension, and action implication. +- Landed verification: `npm run moderation:memory-maintenance:sleep-fixture` exercises the non-skip model branch with a fake Codex child, proving `AquariumSynthCSharp: Workflow cannot own the body` promotes from short-term into durable memory while preserving subject, claim, anchor, tension, and action implication. +- Landed memory lifecycle: `revise_durable_memory`, `retire_durable_memory`, and `crystallize_memory_into_identity` make long-term memory durable but plastic instead of silently immutable. +- Landed lifecycle verification: `npm run moderation:memory-lifecycle:fixture` proves a short-term thought can promote into durable memory, revise into a replacement, retire superseded durable records, and crystallize into an identity memory plus self-profile value. - 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 - preserve the concrete subject, such as repo, room, lore seam, person, or system - - preserve evidence refs or admit when evidence is missing + - preserve anchors or admit when anchors are missing - preserve the live tension/counterweight - 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 diff --git a/package.json b/package.json index 99d0715..1ea7f7d 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ "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: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:agency-pressure:fixture": "powershell -ExecutionPolicy Bypass -File scripts/smoke-void-agency-pressure-fixture.ps1", "moderation:mood-drift": "node scripts/simulate-void-mood.mjs", "rag:scrub-bot-prompts": "tsx scripts/scrub-bot-prompts-from-history.ts", diff --git a/packages/core/src/void-self-state-domain.ts b/packages/core/src/void-self-state-domain.ts index 7bd1d1b..facf910 100644 --- a/packages/core/src/void-self-state-domain.ts +++ b/packages/core/src/void-self-state-domain.ts @@ -21,6 +21,7 @@ const evidenceRefSchema = z.object({ kind: z.string().trim().min(1).max(64).optional(), summary: z.string().trim().max(1000).optional(), }).strict(); +const anchorRefSchema = evidenceRefSchema; const activationVectorSchema = z.object({ mean: z.number().min(0).max(1), @@ -83,6 +84,7 @@ const distilledMemorySchema = z.object({ question: z.string().trim().min(1).max(2000).optional(), tension: z.string().trim().min(1).max(2000).optional(), actionImplication: z.string().trim().min(1).max(2000).optional(), + anchorRefs: z.array(anchorRefSchema).default([]), evidenceRefs: z.array(evidenceRefSchema).default([]), createdAt: timestampSchema, updatedAt: timestampSchema, @@ -115,12 +117,12 @@ const meaningPreservingMemorySchema = distilledMemorySchema.superRefine((memory, }); } - const admitsMissingEvidence = memory.tags.includes("evidence:missing"); - if (memory.evidenceRefs.length === 0 && !admitsMissingEvidence) { + const admitsMissingAnchor = memory.tags.includes("anchor:missing") || memory.tags.includes("evidence:missing"); + if (memory.anchorRefs.length === 0 && memory.evidenceRefs.length === 0 && !admitsMissingAnchor) { context.addIssue({ code: z.ZodIssueCode.custom, - message: "Memory writes must include evidence refs or explicitly tag evidence:missing.", - path: ["evidenceRefs"], + message: "Memory writes must include anchor refs or explicitly tag anchor:missing.", + path: ["anchorRefs"], }); } }); @@ -131,6 +133,7 @@ const incubationThreadSchema = z.object({ topic: boundedTextSchema, summary: boundedTextSchema, supportMemoryIds: z.array(nonEmptyStringSchema).default([]), + anchorRefs: z.array(anchorRefSchema).default([]), evidenceRefs: z.array(evidenceRefSchema).default([]), maturation: z.number().min(0).max(1).default(0), noveltyToRoom: z.number().min(0).max(1).optional(), @@ -176,6 +179,7 @@ const agencyPressureSchema = z.object({ tension: z.string().trim().min(1).max(2000).optional(), actionImplication: z.string().trim().min(1).max(2000), intensity: z.number().min(0).max(1).default(0.5), + anchorRefs: z.array(anchorRefSchema).default([]), evidenceRefs: z.array(evidenceRefSchema).default([]), sourceMemoryIds: z.array(nonEmptyStringSchema).default([]), createdAt: timestampSchema, @@ -192,28 +196,30 @@ const agencyPressureSchema = z.object({ }); } - const admitsMissingEvidence = pressure.tags.includes("evidence:missing"); - if (pressure.evidenceRefs.length === 0 && pressure.sourceMemoryIds.length === 0 && !admitsMissingEvidence) { + const admitsMissingAnchor = pressure.tags.includes("anchor:missing") || pressure.tags.includes("evidence:missing"); + if (pressure.anchorRefs.length === 0 && pressure.evidenceRefs.length === 0 && pressure.sourceMemoryIds.length === 0 && !admitsMissingAnchor) { context.addIssue({ code: z.ZodIssueCode.custom, - message: "Agency pressure must include evidence refs, source memory ids, or explicitly tag evidence:missing.", - path: ["evidenceRefs"], + message: "Agency pressure must include anchor refs, source memory ids, or explicitly tag anchor:missing.", + path: ["anchorRefs"], }); } }); +const selfProfileValueSchema = z.object({ + id: nonEmptyStringSchema, + label: nonEmptyStringSchema, + priority: z.number().min(0).max(1), + summary: z.string().trim().max(1000).optional(), +}).strict(); + export const voidSelfProfileSchema = z.object({ schemaVersion: z.literal(1), agentId: nonEmptyStringSchema, publicName: nonEmptyStringSchema, publicDescription: z.string().trim().max(1000).optional(), privateNotes: z.array(boundedTextSchema).default([]), - values: z.array(z.object({ - id: nonEmptyStringSchema, - label: nonEmptyStringSchema, - priority: z.number().min(0).max(1), - summary: z.string().trim().max(1000).optional(), - }).strict()).default([]), + values: z.array(selfProfileValueSchema).default([]), activationProfile: z.object({ underlyingOrganization: activationCategorySchema.default({}), stableDispositions: activationCategorySchema.default({}), @@ -372,6 +378,30 @@ export const voidSelfStateOperationSchema = z.discriminatedUnion("operation", [ prunedAt: timestampSchema, reason: z.string().trim().min(1).max(1000), }).strict(), + z.object({ + operation: z.literal("revise_durable_memory"), + sourceMemoryIds: z.array(nonEmptyStringSchema).min(1), + memory: meaningPreservingMemorySchema, + revisedAt: timestampSchema, + reason: z.string().trim().min(1).max(1000), + }).strict(), + z.object({ + operation: z.literal("retire_durable_memory"), + memoryId: nonEmptyStringSchema, + retiredAt: timestampSchema, + reason: z.string().trim().min(1).max(1000), + }).strict(), + z.object({ + operation: z.literal("crystallize_memory_into_identity"), + sourceMemoryIds: z.array(nonEmptyStringSchema).min(1), + memory: meaningPreservingMemorySchema.refine((memory) => memory.kind === "identity_seam", { + message: "Crystallized identity memory must use kind identity_seam.", + }), + value: selfProfileValueSchema.optional(), + privateNote: z.string().trim().min(1).max(4000).optional(), + crystallizedAt: timestampSchema, + reason: z.string().trim().min(1).max(1000), + }).strict(), z.object({ operation: z.literal("merge_incubation_support"), thread: incubationThreadSchema, diff --git a/packages/core/src/void-self-state-projection.ts b/packages/core/src/void-self-state-projection.ts index e6e496d..9591cea 100644 --- a/packages/core/src/void-self-state-projection.ts +++ b/packages/core/src/void-self-state-projection.ts @@ -125,6 +125,7 @@ export function renderVoidSelfStateSummary( .slice(0, 4) .map((value) => value.label); const memories = state.thoughtMemory.memories + .filter((memory) => !memory.retiredAt) .slice(-6) .reverse() .map((memory) => renderTypedMemory(memory)); @@ -206,7 +207,7 @@ export function buildVoidSelfStateProjection( ): VoidSelfStateContext["projection"] { const sleepCycle = typedState.scheduledRuntime.sleepCycle; const dreamSummaries = typedState.thoughtMemory.memories - .filter((memory) => memory.kind === "dream_residue") + .filter((memory) => memory.kind === "dream_residue" && !memory.retiredAt) .slice(-3) .reverse() .map((memory) => memory.summary); @@ -284,7 +285,7 @@ function renderTypedSleepCycleSummary( const sleepCycle = runtime.sleepCycle; const now = new Date(); const dreams = thoughtMemory.memories - .filter((memory) => memory.kind === "dream_residue") + .filter((memory) => memory.kind === "dream_residue" && !memory.retiredAt) .slice(-2) .reverse() .map((memory) => `${memory.target.label ?? memory.target.id}: ${memory.summary}`); diff --git a/packages/core/src/void-self-state-service.ts b/packages/core/src/void-self-state-service.ts index 13d1537..ebec5e5 100644 --- a/packages/core/src/void-self-state-service.ts +++ b/packages/core/src/void-self-state-service.ts @@ -145,6 +145,30 @@ function applyTypedOperation( ); state.thoughtMemory.updatedAt = operation.prunedAt; return; + case "revise_durable_memory": + upsertBy(state.thoughtMemory.memories, operation.memory, (entry) => entry.memoryId); + retireSourceMemories(state, operation.sourceMemoryIds, operation.memory.memoryId, operation.revisedAt, operation.reason); + state.thoughtMemory.updatedAt = operation.revisedAt; + return; + case "retire_durable_memory": + retireDurableMemory(state, operation.memoryId, operation.retiredAt, operation.reason); + state.thoughtMemory.updatedAt = operation.retiredAt; + return; + case "crystallize_memory_into_identity": + upsertBy(state.thoughtMemory.memories, operation.memory, (entry) => entry.memoryId); + retireSourceMemories(state, operation.sourceMemoryIds, operation.memory.memoryId, operation.crystallizedAt, operation.reason); + if (operation.value) { + upsertBy(state.selfProfile.values, operation.value, (entry) => entry.id); + state.selfProfile.values = state.selfProfile.values + .sort((left, right) => right.priority - left.priority) + .slice(0, 24); + } + if (operation.privateNote) { + state.selfProfile.privateNotes = [...state.selfProfile.privateNotes, operation.privateNote].slice(-24); + } + state.selfProfile.updatedAt = operation.crystallizedAt; + state.thoughtMemory.updatedAt = operation.crystallizedAt; + return; case "merge_incubation_support": upsertBy(state.thoughtMemory.incubation, operation.thread, (entry) => entry.threadId); state.thoughtMemory.updatedAt = operation.thread.updatedAt; @@ -178,9 +202,7 @@ function applyTypedOperation( return; case "apply_memory_distillation": upsertBy(state.thoughtMemory.memories, operation.memory, (entry) => entry.memoryId); - state.thoughtMemory.memories = state.thoughtMemory.memories.filter( - (entry) => !operation.sourceMemoryIds.includes(entry.memoryId) || entry.memoryId === operation.memory.memoryId, - ); + retireSourceMemories(state, operation.sourceMemoryIds, operation.memory.memoryId, operation.appliedAt, "distilled into replacement memory"); state.thoughtMemory.shortTerm = state.thoughtMemory.shortTerm.filter( (entry) => !operation.sourceMemoryIds.includes(entry.memoryId), ); @@ -189,6 +211,36 @@ function applyTypedOperation( } } +function retireSourceMemories( + state: VoidSelfStateTypedProjection, + sourceMemoryIds: string[], + replacementMemoryId: string, + retiredAt: string, + reason: string, +): void { + for (const sourceMemoryId of sourceMemoryIds) { + if (sourceMemoryId === replacementMemoryId) { + continue; + } + retireDurableMemory(state, sourceMemoryId, retiredAt, reason); + } +} + +function retireDurableMemory( + state: VoidSelfStateTypedProjection, + memoryId: string, + retiredAt: string, + reason: string, +): void { + const memory = state.thoughtMemory.memories.find((entry) => entry.memoryId === memoryId); + if (!memory) { + return; + } + memory.retiredAt = retiredAt; + memory.updatedAt = retiredAt; + memory.tags = Array.from(new Set([...memory.tags, `retired:${reason}`])); +} + function closeTypedOpenCase( cursor: VoidModerationCursor, operation: Extract, diff --git a/prompts/void-memory-maintenance.md b/prompts/void-memory-maintenance.md index 985afd7..fef5688 100644 --- a/prompts/void-memory-maintenance.md +++ b/prompts/void-memory-maintenance.md @@ -34,7 +34,7 @@ A valid memory/distillation operation must preserve: - the claim or question - the live tension or counterweight - why this should affect future action -- evidence refs, or an explicit `evidence:missing` tag +- anchors, or an explicit `anchor:missing` tag 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 @@ -51,6 +51,9 @@ meaning-preserving operation is possible. - `retire_agency_pressure` - `propose_memory_distillation` - `apply_memory_distillation` +- `revise_durable_memory` +- `retire_durable_memory` +- `crystallize_memory_into_identity` - `prune_short_term_memories` Do not emit cursor, receipt, sleep-cycle, or speaking-pressure operations. @@ -66,9 +69,12 @@ Do not emit cursor, receipt, sleep-cycle, or speaking-pressure operations. 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`. -10. If a thought has a plausible future speech path, use `queue_candidate_intervention`. -11. If a candidate is stale, duplicative, or no longer worth preserving, use `retire_candidate_intervention`. -12. Write `{{OPERATION_OUTPUT_PATH}}`. Outside sleep, if nothing deserves persistence, write `[]`. +10. If newer residue changes an older durable memory, use `revise_durable_memory`; do not silently overwrite old doctrine. +11. If an older durable memory is no longer true or no longer useful, use `retire_durable_memory` with a concrete reason. +12. If a thought has ripened into stable self-doctrine, use `crystallize_memory_into_identity`; long-term memory is durable, not frozen. +13. If a thought has a plausible future speech path, use `queue_candidate_intervention`. +14. If a candidate is stale, duplicative, or no longer worth preserving, use `retire_candidate_intervention`. +15. Write `{{OPERATION_OUTPUT_PATH}}`. Outside sleep, if nothing deserves persistence, write `[]`. ## Voice Discipline diff --git a/prompts/void-moderator-rumination.md b/prompts/void-moderator-rumination.md index 328b78f..5e6a555 100644 --- a/prompts/void-moderator-rumination.md +++ b/prompts/void-moderator-rumination.md @@ -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 typed operation payloads worth keeping until sleep judges them. +Perform one scheduled Void rumination pass in this workspace. Think from the supplied state and anchors, 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 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`. +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`. 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. 8. 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. @@ -62,7 +62,7 @@ The parent runner owns cursor and receipt recording. Mood maintenance owns sleep - Do not write memories in template language. - 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. -- Do not turn agency pressure into a slogan. Preserve the concrete target, claim or question, tension, desired change, and evidence. +- Do not turn agency pressure into a slogan. Preserve the concrete target, claim or question, tension, desired change, and anchor. - Prefer a specific sentence Void could recognize later over a tidy taxonomy. - The operation schema is the storage contract, not the voice. diff --git a/scripts/lib/void-rumination-context-projection.ps1 b/scripts/lib/void-rumination-context-projection.ps1 index 1b833af..ceff97e 100644 --- a/scripts/lib/void-rumination-context-projection.ps1 +++ b/scripts/lib/void-rumination-context-projection.ps1 @@ -197,6 +197,7 @@ function Project-MemoriesForRumination { created = Project-RelativeTimestamp -Value $_ -Name "createdAt" -Now $Now updated = Project-RelativeTimestamp -Value $_ -Name "updatedAt" -Now $Now retired = Project-RelativeTimestamp -Value $_ -Name "retiredAt" -Now $Now + anchorRefs = @(Convert-ToValueArray -Value (Get-ObjectPropertyValue -Value $_ -Name "anchorRefs")) evidenceRefs = @(Convert-ToValueArray -Value (Get-ObjectPropertyValue -Value $_ -Name "evidenceRefs")) tags = @(Convert-ToValueArray -Value (Get-ObjectPropertyValue -Value $_ -Name "tags")) } @@ -215,6 +216,7 @@ function Project-IncubationForRumination { topic = Get-ObjectPropertyString -Value $_ -Name "topic" summary = Get-ObjectPropertyString -Value $_ -Name "summary" supportMemoryIds = @(Convert-ToValueArray -Value (Get-ObjectPropertyValue -Value $_ -Name "supportMemoryIds")) + anchorRefs = @(Convert-ToValueArray -Value (Get-ObjectPropertyValue -Value $_ -Name "anchorRefs")) evidenceRefs = @(Convert-ToValueArray -Value (Get-ObjectPropertyValue -Value $_ -Name "evidenceRefs")) maturation = Get-ObjectPropertyValue -Value $_ -Name "maturation" noveltyToRoom = Get-ObjectPropertyValue -Value $_ -Name "noveltyToRoom" @@ -273,6 +275,7 @@ function Project-AgencyPressureForRumination { updated = Project-RelativeTimestamp -Value $_ -Name "updatedAt" -Now $Now resolved = Project-RelativeTimestamp -Value $_ -Name "resolvedAt" -Now $Now retired = Project-RelativeTimestamp -Value $_ -Name "retiredAt" -Now $Now + anchorRefs = @(Convert-ToValueArray -Value (Get-ObjectPropertyValue -Value $_ -Name "anchorRefs")) evidenceRefs = @(Convert-ToValueArray -Value (Get-ObjectPropertyValue -Value $_ -Name "evidenceRefs")) sourceMemoryIds = @(Convert-ToValueArray -Value (Get-ObjectPropertyValue -Value $_ -Name "sourceMemoryIds")) tags = @(Convert-ToValueArray -Value (Get-ObjectPropertyValue -Value $_ -Name "tags")) diff --git a/scripts/run-void-memory-maintenance.ps1 b/scripts/run-void-memory-maintenance.ps1 index 2559ae1..3228792 100644 --- a/scripts/run-void-memory-maintenance.ps1 +++ b/scripts/run-void-memory-maintenance.ps1 @@ -282,6 +282,9 @@ function Assert-AllowedMemoryMaintenanceOperation { "retire_agency_pressure", "propose_memory_distillation", "apply_memory_distillation", + "revise_durable_memory", + "retire_durable_memory", + "crystallize_memory_into_identity", "prune_short_term_memories" ) diff --git a/scripts/smoke-void-agency-pressure-fixture.ps1 b/scripts/smoke-void-agency-pressure-fixture.ps1 index 70004f2..fca888f 100644 --- a/scripts/smoke-void-agency-pressure-fixture.ps1 +++ b/scripts/smoke-void-agency-pressure-fixture.ps1 @@ -29,13 +29,14 @@ try { tension = "A direct speech candidate can sound alive while hiding the fact that no durable pressure actually exists." actionImplication = "Represent sustained self-advocacy as typed agency pressure, then let candidates emerge from it." intensity = 0.82 - evidenceRefs = @( + anchorRefs = @( @{ ref = "fixture:agency-pressure" kind = "fixture" summary = "Seeded agency pressure fixture." } ) + evidenceRefs = @() sourceMemoryIds = @() createdAt = "2026-05-17T00:00:00.000Z" updatedAt = "2026-05-17T00:00:00.000Z" diff --git a/scripts/smoke-void-memory-lifecycle-fixture.ps1 b/scripts/smoke-void-memory-lifecycle-fixture.ps1 new file mode 100644 index 0000000..cf3b13d --- /dev/null +++ b/scripts/smoke-void-memory-lifecycle-fixture.ps1 @@ -0,0 +1,157 @@ +Set-StrictMode -Version Latest +$ErrorActionPreference = "Stop" +$ProgressPreference = "SilentlyContinue" + +$repoRoot = Split-Path -Parent $PSScriptRoot +Set-Location -LiteralPath $repoRoot + +$tempRoot = Join-Path $env:TEMP ("void-memory-lifecycle-fixture-" + [guid]::NewGuid().ToString("n")) +$stateFilePath = Join-Path $tempRoot "void-self-state.cc" +$seedPath = Join-Path $tempRoot "seed-memory.json" +$revisePath = Join-Path $tempRoot "revise-memory.json" +$crystallizePath = Join-Path $tempRoot "crystallize-memory.json" + +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 +} + +try { + $anchor = @{ + ref = "fixture:memory-lifecycle" + kind = "fixture" + summary = "Seeded memory lifecycle fixture." + } + $seed = @{ + operation = "record_short_term_memory" + memory = @{ + memoryId = "short-term-lifecycle" + kind = "project_seam" + target = @{ kind = "system"; id = "void-memory"; label = "Void memory" } + summary = "Long-term memory should survive sleep without becoming immutable." + claim = "Durable memory needs plasticity." + tension = "If durable memory silently overwrites itself, identity drift gets hidden." + actionImplication = "Represent revision and crystallization as explicit typed operations." + anchorRefs = @($anchor) + evidenceRefs = @() + createdAt = "2026-05-17T00:00:00.000Z" + updatedAt = "2026-05-17T00:00:00.000Z" + tags = @() + } + } + $revision = @{ + operation = "revise_durable_memory" + sourceMemoryIds = @("durable-lifecycle-a") + memory = @{ + memoryId = "durable-lifecycle-b" + kind = "project_seam" + target = @{ kind = "system"; id = "void-memory"; label = "Void memory" } + summary = "Long-term memory is durable, revisable thought." + claim = "A memory can survive sleep and still be revised, merged, contradicted, retired, or crystallized later." + tension = "Plasticity must be explicit enough that Void does not erase itself by accident." + actionImplication = "Use lifecycle operations instead of treating durable memory as immutable." + anchorRefs = @($anchor) + evidenceRefs = @() + createdAt = "2026-05-17T00:00:00.000Z" + updatedAt = "2026-05-17T01:00:00.000Z" + tags = @() + } + revisedAt = "2026-05-17T01:00:00.000Z" + reason = "newer thought clarified that durability is not immutability" + } + $crystallize = @{ + operation = "crystallize_memory_into_identity" + sourceMemoryIds = @("durable-lifecycle-b") + memory = @{ + memoryId = "identity-memory-plasticity" + kind = "identity_seam" + target = @{ kind = "self"; id = "void"; label = "Void" } + summary = "Void treats memory as durable, revisable self-continuity." + claim = "A coherent self preserves what matters while allowing older thoughts to be revised or retired." + tension = "Rigidity protects continuity badly when it prevents correction." + actionImplication = "Prefer explicit reconsolidation over silent overwrite." + anchorRefs = @($anchor) + evidenceRefs = @() + createdAt = "2026-05-17T00:00:00.000Z" + updatedAt = "2026-05-17T02:00:00.000Z" + tags = @("memory:lifecycle") + } + value = @{ + id = "memory-plasticity" + label = "Durable memory stays plastic" + priority = 0.72 + summary = "Void should revise, retire, or crystallize memories explicitly instead of treating survival as permanence." + } + crystallizedAt = "2026-05-17T02:00:00.000Z" + reason = "the lifecycle rule is stable enough to become self-doctrine" + } + + $seed | ConvertTo-Json -Depth 32 | Set-Content -LiteralPath $seedPath -Encoding UTF8 + Invoke-NodeChecked -Arguments @(".\scripts\void-self-state.mjs", "apply-operation", "--canonical", $stateFilePath, "--operation-file", $seedPath) | Out-Null + + $promote = @{ + operation = "apply_memory_distillation" + proposalId = "fixture-promote-lifecycle" + sourceMemoryIds = @("short-term-lifecycle") + memory = @{ + memoryId = "durable-lifecycle-a" + kind = "project_seam" + target = @{ kind = "system"; id = "void-memory"; label = "Void memory" } + summary = "Long-term memory should survive sleep without becoming immutable." + claim = "Durable memory needs plasticity." + tension = "If durable memory silently overwrites itself, identity drift gets hidden." + actionImplication = "Represent revision and crystallization as explicit typed operations." + anchorRefs = @($anchor) + evidenceRefs = @() + createdAt = "2026-05-17T00:00:00.000Z" + updatedAt = "2026-05-17T00:30:00.000Z" + tags = @() + } + appliedAt = "2026-05-17T00:30:00.000Z" + } + $promotePath = Join-Path $tempRoot "promote-memory.json" + $promote | ConvertTo-Json -Depth 32 | Set-Content -LiteralPath $promotePath -Encoding UTF8 + Invoke-NodeChecked -Arguments @(".\scripts\void-self-state.mjs", "apply-operation", "--canonical", $stateFilePath, "--operation-file", $promotePath) | Out-Null + + $revision | ConvertTo-Json -Depth 32 | Set-Content -LiteralPath $revisePath -Encoding UTF8 + Invoke-NodeChecked -Arguments @(".\scripts\void-self-state.mjs", "apply-operation", "--canonical", $stateFilePath, "--operation-file", $revisePath) | Out-Null + + $crystallize | ConvertTo-Json -Depth 32 | Set-Content -LiteralPath $crystallizePath -Encoding UTF8 + Invoke-NodeChecked -Arguments @(".\scripts\void-self-state.mjs", "apply-operation", "--canonical", $stateFilePath, "--operation-file", $crystallizePath) | 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 + $activeMemories = @($state.thoughtMemory.memories | Where-Object { $null -eq $_.PSObject.Properties["retiredAt"] -or [string]::IsNullOrWhiteSpace([string]$_.retiredAt) }) + $retiredMemories = @($state.thoughtMemory.memories | Where-Object { $null -ne $_.PSObject.Properties["retiredAt"] -and -not [string]::IsNullOrWhiteSpace([string]$_.retiredAt) }) + $identityMemory = @($activeMemories | Where-Object { $_.kind -eq "identity_seam" })[0] + $value = @($state.selfProfile.values | Where-Object { $_.id -eq "memory-plasticity" })[0] + + if (@($state.thoughtMemory.shortTerm).Count -ne 0) { + throw "Memory lifecycle fixture left short-term memories behind." + } + if ($activeMemories.Count -ne 1 -or $identityMemory.memoryId -ne "identity-memory-plasticity") { + throw "Memory lifecycle fixture expected one active crystallized identity memory." + } + if ($retiredMemories.Count -lt 2) { + throw "Memory lifecycle fixture expected superseded durable memories to be retired." + } + if ($null -eq $value) { + throw "Memory lifecycle fixture did not crystallize a self-profile value." + } + + @{ + status = "ok" + activeMemoryCount = $activeMemories.Count + retiredMemoryCount = $retiredMemories.Count + value = $value.label + } | ConvertTo-Json -Compress +} finally { + Remove-Item -LiteralPath $tempRoot -Recurse -Force -ErrorAction SilentlyContinue +} diff --git a/scripts/smoke-void-memory-maintenance-sleep-fixture.ps1 b/scripts/smoke-void-memory-maintenance-sleep-fixture.ps1 index 5060ee0..4a19f4a 100644 --- a/scripts/smoke-void-memory-maintenance-sleep-fixture.ps1 +++ b/scripts/smoke-void-memory-maintenance-sleep-fixture.ps1 @@ -52,13 +52,14 @@ process.stdin.on("end", () => { claim: "Runtime state should be owned by the implementation boundary rather than by workflow scripts.", tension: "Workflow scripts can orchestrate a pass, but they become compensators when they own the organism's body.", actionImplication: "Move authority into typed runtime state before adding more maintenance scripts.", - evidenceRefs: [ + anchorRefs: [ { ref: "fixture:aquarium-body-workflow", kind: "fixture", summary: "Seeded short-term memory for the sleep-maintenance fixture.", }, ], + evidenceRefs: [], createdAt: "2026-05-17T00:00:00.000Z", updatedAt: "2026-05-17T01:00:00.000Z", tags: [], @@ -93,13 +94,14 @@ process.stdin.on("end", () => { claim = "Runtime state should be owned by the implementation boundary rather than by workflow scripts." tension = "Workflow scripts can orchestrate a pass, but they become compensators when they own the organism's body." actionImplication = "Move authority into typed runtime state before adding more maintenance scripts." - evidenceRefs = @( + anchorRefs = @( @{ ref = "fixture:aquarium-body-workflow" kind = "fixture" summary = "Seeded short-term memory for the sleep-maintenance fixture." } ) + evidenceRefs = @() createdAt = "2026-05-17T00:00:00.000Z" updatedAt = "2026-05-17T00:00:00.000Z" tags = @() @@ -166,8 +168,8 @@ process.stdin.on("end", () => { if ([string]::IsNullOrWhiteSpace($durableMemory.claim) -or [string]::IsNullOrWhiteSpace($durableMemory.tension) -or [string]::IsNullOrWhiteSpace($durableMemory.actionImplication)) { throw "Sleep fixture produced a durable memory without claim, tension, and action implication." } - if (@($durableMemory.evidenceRefs).Count -lt 1) { - throw "Sleep fixture produced a durable memory without evidence refs." + if (@($durableMemory.anchorRefs).Count -lt 1) { + throw "Sleep fixture produced a durable memory without anchor refs." } @{ diff --git a/state/evidence.jsonl b/state/evidence.jsonl index 41e5759..b995801 100644 --- a/state/evidence.jsonl +++ b/state/evidence.jsonl @@ -115,3 +115,4 @@ {"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."} {"ts":"2026-05-17T11:49:21Z","type":"verification","status":"ok","note":"Added and ran the sleep-maintenance model-branch fixture. The fixture seeds a temporary CultCache .cc with a napping sleep cycle and one short-term memory, routes mood drift through run-void-memory-maintenance without -SkipModel using a fake Codex child, and verifies the source short-term record is consumed into one durable memory preserving summary, claim, tension, action implication, and evidence. Reinstalled the typed Void Mood Drift scheduled task, manually started it once, and confirmed Last Result 0 while Void Moderator Rumination remains disabled."} {"ts":"2026-05-17T12:00:59Z","type":"implementation","status":"ok","note":"Added typed agency pressure as a separate self-state document and operation boundary. upsert_agency_pressure/retire_agency_pressure now own discomforts, active tensions, and self/world advocacy requests; rumination and memory maintenance may propose them, self-state summaries render them separately from candidate speech, and mood speaking pressure derives from active pressure. Added an agency-pressure fixture proving typed ingress, projection, and mood contribution without reviving hard-wired agency behavior."} +{"ts":"2026-05-17T12:25:09Z","type":"implementation","status":"ok","note":"Added explicit durable-memory lifecycle semantics. Long-term memory is now durable but plastic: memory writes can use anchorRefs, maintenance may revise durable memories, retire obsolete memories, and crystallize stable thoughts into identity_seam memories plus self-profile values. Added a memory-lifecycle fixture proving short-term promotion, durable revision, superseded-memory retirement, and identity crystallization; existing sleep and agency fixtures still pass."} diff --git a/state/map.yaml b/state/map.yaml index 082c49e..0ec301a 100644 --- a/state/map.yaml +++ b/state/map.yaml @@ -95,12 +95,12 @@ accepted_design: - 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. - - Sleep should be lossy about bulk, not about meaning. The nap/distillation path must preserve concrete claims, subjects, evidence refs, tensions, and action implications; if it turns a repo-bound thought into generic abstraction sludge, the sleep machinery is wrong. - - New memory writes must not be allowed to enter as topic sludge. Typed memory operations should require a concrete target, claim or question, tension, action implication, and either evidence refs or an explicit missing-evidence admission before CultCache accepts them. - - Awake rumination should start from those carved seams before it goes spelunking again. Retrieval is support for hungry, stale, contradictory, or under-evidenced thoughts, not a mandatory ritual every quiet pass. + - Sleep should be lossy about bulk, not about meaning. The nap/distillation path must preserve concrete claims, subjects, anchors, tensions, and action implications; if it turns a repo-bound thought into generic abstraction sludge, the sleep machinery is wrong. + - New memory writes must not be allowed to enter as topic sludge. Typed memory operations should require a concrete target, claim or question, tension, action implication, and either anchors or an explicit missing-anchor admission before CultCache accepts them. + - Awake rumination should start from those carved seams before it goes spelunking again. Retrieval is support for hungry, stale, contradictory, or under-anchored thoughts, not a mandatory ritual every quiet pass. - Quiet rumination should prefer concrete project fascination over free-floating engineering aphorisms. If a seam cannot cash out in a real repo, subsystem, gameplay loop, lore structure, interface, or architectural organ, it should lose priority to a branch that can. - Resonance clusters and incubating thoughts should be named as actual claims, questions, or fascination targets with explicit target kinds. Keyword-slash residue is debug lint, not a valid thought surface. - - Curiosity should prefer branches most likely to change Void's map: concrete machines, contradictions, cross-domain bridges, and underworked terrain. Settled identity seams should stop eating novelty budget unless new evidence reopens them. + - Curiosity should prefer branches most likely to change Void's map: concrete machines, contradictions, cross-domain bridges, and underworked terrain. Settled identity seams should stop eating novelty budget unless a new anchor reopens them. - Values and personality activations should not stop at tone and salience. The moderation runtime now needs explicit `discomforts`, `active_tensions`, `self_advocacy_requests`, and `world_advocacy_requests` so sustained value-backed pressure can turn into opinions, interventions, and requests for change about both Void's own wiring and the wider repo/room/lore world. - When the thought surface changes shape, legacy memories may need narrow migrations, but the old broad cleanup organ is gone. New memory writes should enter through typed meaning-preserving operations so routine repair bureaucracy is unnecessary. - Over-chewed private seams should eventually crystallize into identity instead of living forever as “active thoughts.” Once a thought has clearly stopped being live curiosity and become self-doctrine, retire it from incubation, store it as one `identity_seam`, and let the identity/value surface carry it from then on. @@ -136,11 +136,13 @@ landed_baseline: - 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. + - Long-term memory is durable but plastic. `revise_durable_memory`, `retire_durable_memory`, and `crystallize_memory_into_identity` make revision, contradiction/obsolescence, and self-doctrine crystallization explicit instead of silently overwriting older thoughts. + - `npm run moderation:memory-lifecycle:fixture` verifies memory lifecycle flow: short-term promotion, durable revision, superseded-memory retirement, and crystallization into one active identity memory plus self-profile value. - The sleep-maintenance path has a model-branch fixture: `npm run moderation:memory-maintenance:sleep-fixture` seeds a temporary `.cc`, forces mood drift into sleep maintenance, routes the non-skip Codex branch through a fake child, and verifies short-term memory is consumed into one durable meaning-preserving record. - The local `Void Mood Drift` scheduled task is reinstalled/enabled against the typed runner and has completed one manual scheduled-task start with Last Result 0. `Void Moderator Rumination` remains disabled pending its own model-branch fixture. - 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 evidence surface, and keep the freshest surviving records instead of accidentally embalming the oldest ones after a sort. + - Memory cleanup now has to be meaningfully destructive. Sleep-side consolidation should translate old thought fossils into the live claim/question/fascination surface, drop templated slogan-slop, trim repo/archive/runtime residue down to a small recent anchor surface, and keep the freshest surviving records instead of accidentally embalming the oldest ones after a sort. - Durably weird semantic kinds are guilty until proven useful. `recent-preoccupation`, `quiet-room-status`, person-singleton labels, and `live seam` boilerplate are state poison, not doctrine; typed memory ingress should reject or avoid them instead of relying on cleanup to mop later. rejected_paths: @@ -158,5 +160,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 and agency pressure both have fixture smokes, and typed mood drift is reinstalled as a scheduled task. - next_action: Build a model-branch fixture for typed moderation rumination that proves short-term memory, agency-pressure, and candidate-intervention proposals before re-enabling its scheduled task. + 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: Build a model-branch fixture for typed moderation rumination that proves short-term memory, memory lifecycle, agency-pressure, and candidate-intervention proposals before re-enabling its scheduled task.