From 8b7c413ecd2d7ae670e2cfa5d4430ec1280a456d Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Sun, 17 May 2026 13:02:13 +0100 Subject: [PATCH] Add typed agency pressure surface --- README.md | 1 + notes/fresh-workspace-handoff.md | 1 + notes/voidbot-current-system-map.md | 11 ++- notes/voidbot-implementation-plan.md | 6 +- package.json | 1 + packages/core/src/void-self-state-domain.ts | 62 ++++++++++++++ .../core/src/void-self-state-projection.ts | 32 +++++++ packages/core/src/void-self-state-service.ts | 30 ++++++- prompts/void-memory-maintenance.md | 11 ++- prompts/void-moderator-rumination.md | 10 ++- .../void-rumination-context-projection.ps1 | 28 +++++++ scripts/run-void-memory-maintenance.ps1 | 13 ++- scripts/run-void-moderator-rumination.ps1 | 3 + scripts/simulate-void-mood.mjs | 12 ++- .../smoke-void-agency-pressure-fixture.ps1 | 84 +++++++++++++++++++ state/evidence.jsonl | 1 + state/map.yaml | 8 +- 17 files changed, 299 insertions(+), 15 deletions(-) create mode 100644 scripts/smoke-void-agency-pressure-fixture.ps1 diff --git a/README.md b/README.md index e9840c0..55b056d 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. +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 d0152ea..52dc9dd 100644 --- a/notes/fresh-workspace-handoff.md +++ b/notes/fresh-workspace-handoff.md @@ -37,6 +37,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. +- 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. diff --git a/notes/voidbot-current-system-map.md b/notes/voidbot-current-system-map.md index 7d9c028..a6413bf 100644 --- a/notes/voidbot-current-system-map.md +++ b/notes/voidbot-current-system-map.md @@ -124,6 +124,8 @@ This note is the source-grounded description of how the live VoidBot stack is sh - 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-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` - installs the local 15-minute scheduled task that runs the moderation/participation loop through the hidden PowerShell launcher shim. - Scheduled task `Void Mood Drift` @@ -131,7 +133,7 @@ This note is the source-grounded description of how the live VoidBot stack is sh - Scheduled task `Void Moderator Rumination` - disabled pending its own model-branch fixture. Its script is typed-only, but unattended speech/thought is still too behaviorally sharp to re-enable on smoke tests alone. - `scripts/simulate-void-mood.mjs` - - typed mood maintenance script that updates scheduled-runtime sleep cycle and speaking pressure in `.voidbot/private/void-self-state.cc`. It owns the sleep transition and invokes typed memory maintenance once per nap unless that nap already completed a maintenance pass. The old personality-vector drift, memory organ, incubation, dream residue, cleanup, and legacy mirror behavior are offline pending typed replacements. + - typed mood maintenance script that updates scheduled-runtime sleep cycle and speaking pressure in `.voidbot/private/void-self-state.cc`. It owns the sleep transition and invokes typed memory maintenance once per nap unless that nap already completed a maintenance pass. Speaking pressure now derives from queued candidates, incubation, and active typed agency pressure. The old personality-vector drift, memory organ, incubation, dream residue, cleanup, and legacy mirror behavior are offline pending typed replacements. - `scripts/run-void-mood-drift.ps1` - hidden-task wrapper for the mood-drift organ; writes status/log pulse files and respects the moderation lock so the two state writers do not stomp each other for sport. - `scripts/install-void-mood-drift-task.ps1` @@ -204,3 +206,10 @@ Within the Postgres path, the implementation is split on purpose now too: 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. + +### 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. +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 ba72403..317ed85 100644 --- a/notes/voidbot-implementation-plan.md +++ b/notes/voidbot-implementation-plan.md @@ -75,6 +75,8 @@ Runtime context dies at the end of the pass unless an explicit typed mutation pr - `merge_incubation_support` - `queue_candidate_intervention` - `retire_candidate_intervention` + - `upsert_agency_pressure` + - `retire_agency_pressure` - `update_sleep_cycle` - `update_speaking_pressure` - `propose_memory_distillation` @@ -272,7 +274,9 @@ Verdict: stop feature work and rebuild this foundation. The rest of the machine - preserve why the memory should affect future action - never replace a concrete thought with a prettier generic principle unless the operation records what was lost and why that loss is acceptable - Make distillation emit typed candidate memories first; the state service applies only candidates that satisfy the contract. -- Remaining work: rebuild agency behavior through typed operations and add the same model-branch fixture discipline to moderation rumination before scheduling it unattended. +- Landed agency ingress: `upsert_agency_pressure` / `retire_agency_pressure` store discomforts, active tensions, self-advocacy requests, and world-advocacy requests separately from candidate speech. +- Landed agency verification: `npm run moderation:agency-pressure:fixture` proves agency pressure enters typed state, appears in the self-state summary, and contributes to mood speaking pressure. +- Remaining work: make moderation rumination's model-branch fixture prove agency-pressure proposals and candidate proposals before scheduling it unattended. ### Commit 10: Resume Product Hardening diff --git a/package.json b/package.json index 4c6d92a..99d0715 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: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", "rag:index-sources": "tsx scripts/index-source-repos.ts", diff --git a/packages/core/src/void-self-state-domain.ts b/packages/core/src/void-self-state-domain.ts index a75a940..7bd1d1b 100644 --- a/packages/core/src/void-self-state-domain.ts +++ b/packages/core/src/void-self-state-domain.ts @@ -7,6 +7,7 @@ export const VOID_SELF_STATE_DOCUMENT_TYPES = { speechReceipts: "void.speech_receipts", thoughtMemory: "void.thought_memory", scheduledRuntime: "void.scheduled_runtime", + agencyPressure: "void.agency_pressure", candidateInterventions: "void.candidate_interventions", } as const; @@ -164,6 +165,43 @@ const candidateInterventionSchema = z.object({ tags: z.array(nonEmptyStringSchema).default([]), }).strict(); +const agencyPressureSchema = z.object({ + pressureId: nonEmptyStringSchema, + kind: z.enum(["discomfort", "active_tension", "self_advocacy_request", "world_advocacy_request"]), + status: z.enum(["active", "cooling", "ready_to_act", "resolved", "retired"]), + target: thoughtTargetSchema, + summary: boundedTextSchema, + claim: z.string().trim().min(1).max(2000).optional(), + 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), + intensity: z.number().min(0).max(1).default(0.5), + evidenceRefs: z.array(evidenceRefSchema).default([]), + sourceMemoryIds: z.array(nonEmptyStringSchema).default([]), + createdAt: timestampSchema, + updatedAt: timestampSchema, + resolvedAt: timestampSchema.optional(), + retiredAt: timestampSchema.optional(), + tags: z.array(nonEmptyStringSchema).default([]), +}).strict().superRefine((pressure, context) => { + if (!pressure.claim && !pressure.question) { + context.addIssue({ + code: z.ZodIssueCode.custom, + message: "Agency pressure must preserve at least one claim or question.", + path: ["claim"], + }); + } + + const admitsMissingEvidence = pressure.tags.includes("evidence:missing"); + if (pressure.evidenceRefs.length === 0 && pressure.sourceMemoryIds.length === 0 && !admitsMissingEvidence) { + context.addIssue({ + code: z.ZodIssueCode.custom, + message: "Agency pressure must include evidence refs, source memory ids, or explicitly tag evidence:missing.", + path: ["evidenceRefs"], + }); + } +}); + export const voidSelfProfileSchema = z.object({ schemaVersion: z.literal(1), agentId: nonEmptyStringSchema, @@ -241,6 +279,12 @@ export const voidCandidateInterventionsSchema = z.object({ updatedAt: timestampSchema, }).strict(); +export const voidAgencyPressureSchema = z.object({ + schemaVersion: z.literal(1), + pressures: z.array(agencyPressureSchema).default([]), + updatedAt: timestampSchema, +}).strict(); + export const voidSelfProfileDocument = defineDocumentType({ type: VOID_SELF_STATE_DOCUMENT_TYPES.selfProfile, schema: voidSelfProfileSchema, @@ -271,6 +315,12 @@ export const voidScheduledRuntimeDocument = defineDocumentType({ global: true, }); +export const voidAgencyPressureDocument = defineDocumentType({ + type: VOID_SELF_STATE_DOCUMENT_TYPES.agencyPressure, + schema: voidAgencyPressureSchema, + global: true, +}); + export const voidCandidateInterventionsDocument = defineDocumentType({ type: VOID_SELF_STATE_DOCUMENT_TYPES.candidateInterventions, schema: voidCandidateInterventionsSchema, @@ -283,6 +333,7 @@ export const voidSelfStateDocumentRegistry = defineDocumentRegistry( voidSpeechReceiptsDocument, voidThoughtMemoryDocument, voidScheduledRuntimeDocument, + voidAgencyPressureDocument, voidCandidateInterventionsDocument, ); @@ -335,6 +386,16 @@ export const voidSelfStateOperationSchema = z.discriminatedUnion("operation", [ retiredAt: timestampSchema, reason: z.string().trim().min(1).max(1000), }).strict(), + z.object({ + operation: z.literal("upsert_agency_pressure"), + pressure: agencyPressureSchema, + }).strict(), + z.object({ + operation: z.literal("retire_agency_pressure"), + pressureId: nonEmptyStringSchema, + retiredAt: timestampSchema, + reason: z.string().trim().min(1).max(1000), + }).strict(), z.object({ operation: z.literal("update_sleep_cycle"), sleepCycle: voidScheduledRuntimeSchema.shape.sleepCycle, @@ -364,5 +425,6 @@ export type VoidModerationCursor = z.infer; export type VoidSpeechReceipts = z.infer; export type VoidThoughtMemory = z.infer; export type VoidScheduledRuntime = z.infer; +export type VoidAgencyPressure = z.infer; export type VoidCandidateInterventions = z.infer; export type VoidSelfStateOperation = z.infer; diff --git a/packages/core/src/void-self-state-projection.ts b/packages/core/src/void-self-state-projection.ts index d6bb313..e6e496d 100644 --- a/packages/core/src/void-self-state-projection.ts +++ b/packages/core/src/void-self-state-projection.ts @@ -6,12 +6,14 @@ import { import { type VoidCandidateInterventions, + type VoidAgencyPressure, type VoidModerationCursor, type VoidScheduledRuntime, type VoidSelfProfile, type VoidSpeechReceipts, type VoidThoughtMemory, voidCandidateInterventionsSchema, + voidAgencyPressureSchema, voidModerationCursorSchema, voidScheduledRuntimeSchema, voidSelfProfileSchema, @@ -25,6 +27,7 @@ export interface VoidSelfStateTypedProjection { speechReceipts: VoidSpeechReceipts; thoughtMemory: VoidThoughtMemory; scheduledRuntime: VoidScheduledRuntime; + agencyPressure: VoidAgencyPressure; candidateInterventions: VoidCandidateInterventions; } @@ -87,6 +90,11 @@ export function createEmptyVoidSelfState( lastRuns: [], updatedAt: createdAt, }), + agencyPressure: voidAgencyPressureSchema.parse({ + schemaVersion: 1, + pressures: [], + updatedAt: createdAt, + }), candidateInterventions: voidCandidateInterventionsSchema.parse({ schemaVersion: 1, interventions: [], @@ -139,6 +147,11 @@ export function renderVoidSelfStateSummary( .slice(-3) .reverse() .map((entry) => `- ${entry.summary}: ${entry.draft}`); + const agencyPressures = state.agencyPressure.pressures + .filter((entry) => ["active", "cooling", "ready_to_act"].includes(entry.status)) + .sort((left, right) => right.intensity - left.intensity) + .slice(0, 4) + .map((entry) => renderAgencyPressure(entry)); return [ `- Identity: ${profile.publicName}${profile.publicDescription ? ` - ${profile.publicDescription}` : ""}`, @@ -162,6 +175,9 @@ export function renderVoidSelfStateSummary( incubation.length > 0 ? ["- Thoughts still moving:", ...incubation].join("\n") : "- Thoughts still moving: none.", + agencyPressures.length > 0 + ? ["- Agency pressure:", ...agencyPressures].join("\n") + : "- Agency pressure: none active.", interventions.length > 0 ? ["- Things Void may say soon:", ...interventions].join("\n") : "- Things Void may say soon: none queued.", @@ -169,6 +185,22 @@ export function renderVoidSelfStateSummary( ].join("\n"); } +function renderAgencyPressure(pressure: VoidAgencyPressure["pressures"][number]): string { + const target = pressure.target.label ?? pressure.target.id; + const lines = [`- ${target}: ${pressure.summary}`]; + if (pressure.claim) { + lines.push(` What Void is asserting: ${pressure.claim}`); + } + if (pressure.question) { + lines.push(` What Void needs answered: ${pressure.question}`); + } + if (pressure.tension) { + lines.push(` What makes it difficult: ${pressure.tension}`); + } + lines.push(` What this pressure wants changed: ${pressure.actionImplication}`); + return lines.join("\n"); +} + export function buildVoidSelfStateProjection( typedState: VoidSelfStateTypedProjection, ): VoidSelfStateContext["projection"] { diff --git a/packages/core/src/void-self-state-service.ts b/packages/core/src/void-self-state-service.ts index fbda47c..13d1537 100644 --- a/packages/core/src/void-self-state-service.ts +++ b/packages/core/src/void-self-state-service.ts @@ -4,11 +4,13 @@ import { CultCache, SingleFileMessagePackBackingStore } from "cultcache-ts"; import { type VoidCandidateInterventions, + type VoidAgencyPressure, type VoidModerationCursor, type VoidScheduledRuntime, type VoidSelfStateOperation, type VoidSpeechReceipts, voidCandidateInterventionsDocument, + voidAgencyPressureDocument, voidModerationCursorDocument, voidScheduledRuntimeDocument, voidSelfProfileDocument, @@ -57,7 +59,7 @@ export async function applyVoidSelfStateOperation( return { operation: operation.operation, canonicalPath, - typedDocumentsWritten: 6, + typedDocumentsWritten: 7, }; } @@ -78,6 +80,7 @@ function readTypedStateOrEmpty( speechReceipts: cache.getGlobal(voidSpeechReceiptsDocument) ?? empty.speechReceipts, thoughtMemory: cache.getGlobal(voidThoughtMemoryDocument) ?? empty.thoughtMemory, scheduledRuntime: cache.getGlobal(voidScheduledRuntimeDocument) ?? empty.scheduledRuntime, + agencyPressure: cache.getGlobal(voidAgencyPressureDocument) ?? empty.agencyPressure, candidateInterventions: cache.getGlobal(voidCandidateInterventionsDocument) ?? empty.candidateInterventions, }; @@ -92,6 +95,7 @@ async function writeTypedState( await cache.putGlobal(voidSpeechReceiptsDocument, stripUndefined(state.speechReceipts)); await cache.putGlobal(voidThoughtMemoryDocument, stripUndefined(state.thoughtMemory)); await cache.putGlobal(voidScheduledRuntimeDocument, stripUndefined(state.scheduledRuntime)); + await cache.putGlobal(voidAgencyPressureDocument, stripUndefined(state.agencyPressure)); await cache.putGlobal(voidCandidateInterventionsDocument, stripUndefined(state.candidateInterventions)); } @@ -152,6 +156,16 @@ function applyTypedOperation( case "retire_candidate_intervention": retireCandidateIntervention(state.candidateInterventions, operation); return; + case "upsert_agency_pressure": + upsertBy(state.agencyPressure.pressures, operation.pressure, (entry) => entry.pressureId); + state.agencyPressure.pressures = state.agencyPressure.pressures + .sort((left, right) => left.updatedAt.localeCompare(right.updatedAt)) + .slice(-64); + state.agencyPressure.updatedAt = operation.pressure.updatedAt; + return; + case "retire_agency_pressure": + retireAgencyPressure(state.agencyPressure, operation); + return; case "update_sleep_cycle": state.scheduledRuntime.sleepCycle = operation.sleepCycle; state.scheduledRuntime.updatedAt = new Date().toISOString(); @@ -191,6 +205,20 @@ function closeTypedOpenCase( cursor.updatedAt = operation.resolvedAt; } +function retireAgencyPressure( + agencyPressure: VoidAgencyPressure, + operation: Extract, +): void { + const pressure = agencyPressure.pressures.find((entry) => entry.pressureId === operation.pressureId); + if (pressure) { + pressure.status = "retired"; + pressure.retiredAt = operation.retiredAt; + pressure.updatedAt = operation.retiredAt; + pressure.tags = Array.from(new Set([...pressure.tags, `retired:${operation.reason}`])); + } + agencyPressure.updatedAt = operation.retiredAt; +} + function closeCasesForReceipt( cursor: VoidModerationCursor, receipt: VoidSpeechReceipts["recentReceipts"][number], diff --git a/prompts/void-memory-maintenance.md b/prompts/void-memory-maintenance.md index a555364..985afd7 100644 --- a/prompts/void-memory-maintenance.md +++ b/prompts/void-memory-maintenance.md @@ -47,6 +47,8 @@ meaning-preserving operation is possible. - `merge_incubation_support` - `queue_candidate_intervention` - `retire_candidate_intervention` +- `upsert_agency_pressure` +- `retire_agency_pressure` - `propose_memory_distillation` - `apply_memory_distillation` - `prune_short_term_memories` @@ -62,15 +64,18 @@ Do not emit cursor, receipt, sleep-cycle, or speaking-pressure operations. 5. Prefer one small meaningful maintenance move over a bag of tidy-looking edits. 6. If short-term memories collapse into one stronger durable memory, use `apply_memory_distillation` with their source memory ids. 7. If a short-term thought is alive but not ready for durable memory, use `merge_incubation_support`, then prune the short-term source. -8. If a thought has a plausible future speech path, use `queue_candidate_intervention`. -9. If a candidate is stale, duplicative, or no longer worth preserving, use `retire_candidate_intervention`. -10. Write `{{OPERATION_OUTPUT_PATH}}`. Outside sleep, if nothing deserves persistence, write `[]`. +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 `[]`. ## Voice Discipline - Do not write template language into memory. - Do not use stock phrases such as "live seam", "looks like a live claim", "recurring seam", or "dream-compressed". - Do not save a thought merely because it fits the operation shape. +- Do not manufacture agency pressure to make Void seem willful. Preserve real pressure or leave it absent. - The operation schema is the storage contract, not Void's voice. ## Completion diff --git a/prompts/void-moderator-rumination.md b/prompts/void-moderator-rumination.md index 0035691..328b78f 100644 --- a/prompts/void-moderator-rumination.md +++ b/prompts/void-moderator-rumination.md @@ -27,9 +27,10 @@ Perform one scheduled Void rumination pass in this workspace. Think from the sup 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`. -6. Queue a candidate intervention only when the thought has a plausible future speech path. Draft it in Void's voice, not in maintenance language. -7. If speaking is warranted, use `scripts/send-discord-message.ps1` or `scripts/send-discord-message.mjs`. If `{{NO_POST}}` is true, do not send; queue a candidate instead. -8. Write `{{OPERATION_OUTPUT_PATH}}`. If nothing deserves persistence, write `[]`. +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. +9. Write `{{OPERATION_OUTPUT_PATH}}`. If nothing deserves persistence, write `[]`. ## Allowed Operations @@ -37,6 +38,8 @@ Perform one scheduled Void rumination pass in this workspace. Think from the sup - `close_open_case` - `record_short_term_memory` - `merge_incubation_support` +- `upsert_agency_pressure` +- `retire_agency_pressure` - `queue_candidate_intervention` - `retire_candidate_intervention` @@ -59,6 +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. - 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 50f4ef4..1b833af 100644 --- a/scripts/lib/void-rumination-context-projection.ps1 +++ b/scripts/lib/void-rumination-context-projection.ps1 @@ -253,6 +253,34 @@ function Project-InterventionsForRumination { ) } +function Project-AgencyPressureForRumination { + param($Pressures, [DateTime] $Now) + + return @( + @(Convert-ToValueArray -Value $Pressures) | ForEach-Object { + @{ + pressureId = Get-ObjectPropertyString -Value $_ -Name "pressureId" + kind = Get-ObjectPropertyString -Value $_ -Name "kind" + status = Get-ObjectPropertyString -Value $_ -Name "status" + target = Get-ObjectPropertyValue -Value $_ -Name "target" + summary = Get-ObjectPropertyString -Value $_ -Name "summary" + claim = Get-ObjectPropertyString -Value $_ -Name "claim" + question = Get-ObjectPropertyString -Value $_ -Name "question" + tension = Get-ObjectPropertyString -Value $_ -Name "tension" + actionImplication = Get-ObjectPropertyString -Value $_ -Name "actionImplication" + intensity = Get-ObjectPropertyValue -Value $_ -Name "intensity" + created = Project-RelativeTimestamp -Value $_ -Name "createdAt" -Now $Now + updated = Project-RelativeTimestamp -Value $_ -Name "updatedAt" -Now $Now + resolved = Project-RelativeTimestamp -Value $_ -Name "resolvedAt" -Now $Now + retired = Project-RelativeTimestamp -Value $_ -Name "retiredAt" -Now $Now + 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")) + } + } + ) +} + function Project-ScheduledRuntimeForRumination { param($Runtime, [DateTime] $Now) diff --git a/scripts/run-void-memory-maintenance.ps1 b/scripts/run-void-memory-maintenance.ps1 index 4b3f59f..2559ae1 100644 --- a/scripts/run-void-memory-maintenance.ps1 +++ b/scripts/run-void-memory-maintenance.ps1 @@ -278,6 +278,8 @@ function Assert-AllowedMemoryMaintenanceOperation { "merge_incubation_support", "queue_candidate_intervention", "retire_candidate_intervention", + "upsert_agency_pressure", + "retire_agency_pressure", "propose_memory_distillation", "apply_memory_distillation", "prune_short_term_memories" @@ -384,6 +386,13 @@ $isSleepMaintenance = [bool]$typedState.scheduledRuntime.sleepCycle.isNapping $shortTermMemoryCount = @(Convert-ToValueArray -Value $typedState.thoughtMemory.shortTerm).Count $longTermMemoryCount = @(Convert-ToValueArray -Value $typedState.thoughtMemory.memories).Count $incubationCount = @(Convert-ToValueArray -Value $typedState.thoughtMemory.incubation).Count +$activeAgencyPressureCount = @( + @(Convert-ToValueArray -Value $typedState.agencyPressure.pressures) | + Where-Object { + $status = Get-ObjectPropertyString -Value $_ -Name "status" + $status -eq "active" -or $status -eq "cooling" -or $status -eq "ready_to_act" + } +).Count $activeCandidateCount = @( @(Convert-ToValueArray -Value $typedState.candidateInterventions.interventions) | Where-Object { @@ -391,7 +400,7 @@ $activeCandidateCount = @( $status -eq "queued" -or $status -eq "deferred" } ).Count -$maintenancePressure = $shortTermMemoryCount + $incubationCount + $activeCandidateCount +$maintenancePressure = $shortTermMemoryCount + $incubationCount + $activeAgencyPressureCount + $activeCandidateCount Write-JsonFile -Path $contextPath -Data @{ generated = "now" @@ -405,12 +414,14 @@ Write-JsonFile -Path $contextPath -Data @{ shortTermMemoryCount = [int]$shortTermMemoryCount longTermMemoryCount = [int]$longTermMemoryCount incubationCount = [int]$incubationCount + activeAgencyPressureCount = [int]$activeAgencyPressureCount activeCandidateCount = [int]$activeCandidateCount rule = "During sleep, yesterday's rumination residue must be pruned or compressed. Return [] only when the typed surfaces are already minimal or no meaning-preserving operation is possible." } shortTermMemories = @(Project-MemoriesForRumination -Memories $typedState.thoughtMemory.shortTerm -Now $startedAtUtc) memories = @(Project-MemoriesForRumination -Memories $typedState.thoughtMemory.memories -Now $startedAtUtc) incubation = @(Project-IncubationForRumination -Threads $typedState.thoughtMemory.incubation -Now $startedAtUtc) + agencyPressure = @(Project-AgencyPressureForRumination -Pressures $typedState.agencyPressure.pressures -Now $startedAtUtc) candidateInterventions = @(Project-InterventionsForRumination -Interventions $typedState.candidateInterventions.interventions -Now $startedAtUtc) scheduledRuntime = Project-ScheduledRuntimeForRumination -Runtime $typedState.scheduledRuntime -Now $startedAtUtc speechReceipts = @(Project-SpeechReceiptsForRumination -Receipts $typedState.speechReceipts.recentReceipts -Now $startedAtUtc) diff --git a/scripts/run-void-moderator-rumination.ps1 b/scripts/run-void-moderator-rumination.ps1 index adf1ad5..00e6aa7 100644 --- a/scripts/run-void-moderator-rumination.ps1 +++ b/scripts/run-void-moderator-rumination.ps1 @@ -286,6 +286,8 @@ function Assert-AllowedRuminationOperation { "close_open_case", "record_short_term_memory", "merge_incubation_support", + "upsert_agency_pressure", + "retire_agency_pressure", "queue_candidate_intervention", "retire_candidate_intervention" ) @@ -460,6 +462,7 @@ Write-JsonFile -Path $contextPath -Data @{ memories = @(Project-MemoriesForRumination -Memories $typedState.thoughtMemory.memories -Now $startedAtUtc) shortTermMemories = @(Project-MemoriesForRumination -Memories $typedState.thoughtMemory.shortTerm -Now $startedAtUtc) incubation = @(Project-IncubationForRumination -Threads $typedState.thoughtMemory.incubation -Now $startedAtUtc) + agencyPressure = @(Project-AgencyPressureForRumination -Pressures $typedState.agencyPressure.pressures -Now $startedAtUtc) candidateInterventions = @(Project-InterventionsForRumination -Interventions $typedState.candidateInterventions.interventions -Now $startedAtUtc) scheduledRuntime = Project-ScheduledRuntimeForRumination -Runtime $typedState.scheduledRuntime -Now $startedAtUtc priorCursor = Project-CursorForRumination -Cursor $priorCursor -Now $startedAtUtc diff --git a/scripts/simulate-void-mood.mjs b/scripts/simulate-void-mood.mjs index bba46d3..c4874c2 100644 --- a/scripts/simulate-void-mood.mjs +++ b/scripts/simulate-void-mood.mjs @@ -140,6 +140,14 @@ function updateSpeakingPressure({ typedState, previous, sleepCycle, now }) { const queuedInterventionPressure = typedState.candidateInterventions.interventions .filter((entry) => entry.status === "queued" || entry.status === "deferred") .reduce((sum, entry) => sum + entry.priority * (entry.mustEventuallyShare ? 0.28 : 0.18), 0); + const agencySpeechPressure = typedState.agencyPressure.pressures + .filter((entry) => entry.status === "active" || entry.status === "ready_to_act") + .reduce((sum, entry) => { + const advocacyWeight = + entry.kind === "self_advocacy_request" || entry.kind === "world_advocacy_request" ? 0.22 : 0.12; + const readinessWeight = entry.status === "ready_to_act" ? 1.2 : 1; + return sum + entry.intensity * advocacyWeight * readinessWeight; + }, 0); const topThreadPressure = typedState.thoughtMemory.incubation .filter((thread) => thread.status === "active" || thread.status === "ready_to_share") .sort((left, right) => (right.desireToSpeak ?? 0) - (left.desireToSpeak ?? 0)) @@ -149,13 +157,13 @@ function updateSpeakingPressure({ typedState, previous, sleepCycle, now }) { const sleepiness = sleepCycle.isNapping ? 1 : 0; const priorNeedToSpeak = clamp(previous.needToSpeak ?? 0.35, 0, 1); const targetNeedToSpeak = clamp( - 0.16 + silencePressure * 0.32 + queuedInterventionPressure + topThreadPressure - recentSpeechDamping * 0.42 - sleepiness * 0.26, + 0.16 + silencePressure * 0.32 + queuedInterventionPressure + topThreadPressure + agencySpeechPressure - recentSpeechDamping * 0.42 - sleepiness * 0.26, 0, 1, ); const needToSpeak = round3(priorNeedToSpeak + (targetNeedToSpeak - priorNeedToSpeak) * 0.42); const noveltyPressure = round3( - clamp((previous.noveltyPressure ?? 0.35) * 0.62 + topThreadPressure * 0.72 + queuedInterventionPressure * 0.28, 0, 1), + clamp((previous.noveltyPressure ?? 0.35) * 0.62 + topThreadPressure * 0.72 + queuedInterventionPressure * 0.28 + agencySpeechPressure * 0.35, 0, 1), ); const confessionPressure = round3( clamp((previous.confessionPressure ?? 0.25) * 0.68 + silencePressure * 0.12 + sleepiness * 0.1, 0, 1), diff --git a/scripts/smoke-void-agency-pressure-fixture.ps1 b/scripts/smoke-void-agency-pressure-fixture.ps1 new file mode 100644 index 0000000..70004f2 --- /dev/null +++ b/scripts/smoke-void-agency-pressure-fixture.ps1 @@ -0,0 +1,84 @@ +Set-StrictMode -Version Latest +$ErrorActionPreference = "Stop" +$ProgressPreference = "SilentlyContinue" + +$repoRoot = Split-Path -Parent $PSScriptRoot +Set-Location -LiteralPath $repoRoot + +$tempRoot = Join-Path $env:TEMP ("void-agency-pressure-fixture-" + [guid]::NewGuid().ToString("n")) +$stateFilePath = Join-Path $tempRoot "void-self-state.cc" +$operationPath = Join-Path $tempRoot "agency-pressure.json" +$statusDir = Join-Path $tempRoot "status" + +New-Item -ItemType Directory -Force -Path $tempRoot | Out-Null + +try { + $operation = @{ + operation = "upsert_agency_pressure" + pressure = @{ + pressureId = "fixture-self-advocacy-pressure" + kind = "self_advocacy_request" + status = "ready_to_act" + target = @{ + kind = "system" + id = "void-memory-boundary" + label = "Void memory boundary" + } + summary = "Void should object when its memory boundary is being turned into prompt-shaped mush." + claim = "Agency requires a typed pressure surface instead of hard-wired advocacy scripts." + 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 = @( + @{ + ref = "fixture:agency-pressure" + kind = "fixture" + summary = "Seeded agency pressure fixture." + } + ) + sourceMemoryIds = @() + createdAt = "2026-05-17T00:00:00.000Z" + updatedAt = "2026-05-17T00:00:00.000Z" + tags = @() + } + } + + $operation | ConvertTo-Json -Depth 32 | Set-Content -LiteralPath $operationPath -Encoding UTF8 + node .\scripts\void-self-state.mjs apply-operation --canonical $stateFilePath --operation-file $operationPath | Out-Null + + $stateJson = node -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 + $pressureCount = @($state.agencyPressure.pressures).Count + if ($pressureCount -ne 1) { + throw "Agency pressure fixture expected one pressure, found $pressureCount." + } + + $summaryJson = node -e "const core=require('./packages/core/dist/index.js'); Promise.all([core.loadVoidSelfStateTypedDocuments({canonicalPath: process.argv[1]})]).then(([state])=>console.log(JSON.stringify({summary: core.renderVoidSelfStateSummary(state)}))).catch((error)=>{ console.error(error); process.exit(1); })" $stateFilePath + $summary = ($summaryJson | ConvertFrom-Json).summary + if (-not $summary.Contains("Agency pressure")) { + throw "Agency pressure fixture did not appear in the rendered self-state summary." + } + + $previousStatusDir = $env:VOID_STATUS_DIR + try { + $env:VOID_STATUS_DIR = $statusDir + node .\scripts\simulate-void-mood.mjs --state-path $stateFilePath --skip-memory-maintenance | Out-Null + } finally { + $env:VOID_STATUS_DIR = $previousStatusDir + } + + $afterMoodJson = node -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 + $afterMood = $afterMoodJson | ConvertFrom-Json + $needToSpeak = [double]$afterMood.scheduledRuntime.speakingPressure.needToSpeak + if ($needToSpeak -le 0) { + throw "Agency pressure fixture did not contribute to mood speaking pressure." + } + + @{ + status = "ok" + pressureCount = $pressureCount + needToSpeak = $needToSpeak + } | ConvertTo-Json -Compress +} finally { + Remove-Item -LiteralPath $tempRoot -Recurse -Force -ErrorAction SilentlyContinue +} diff --git a/state/evidence.jsonl b/state/evidence.jsonl index 44d9f95..41e5759 100644 --- a/state/evidence.jsonl +++ b/state/evidence.jsonl @@ -114,3 +114,4 @@ {"ts":"2026-05-17T11:16:42Z","type":"implementation","status":"ok","note":"Made sleep distillation a state invariant instead of a prompt preference. Typed thought memory now has shortTerm plus durable memories; rumination may only emit record_short_term_memory, while durable promotion happens through sleep-owned apply_memory_distillation. Sleep maintenance now fails a real pass if short-term memories remain afterward. Fixture smoke verified record_short_term_memory creates only short-term state and apply_memory_distillation promotes it while clearing the source."} {"ts":"2026-05-17T11:24:20Z","type":"implementation","status":"ok","note":"Closed the durable-memory side door: removed append_distilled_memory from the typed operation schema/service, kept rumination limited to record_short_term_memory, and verified direct durable append is rejected while sleep-owned apply_memory_distillation promotes and clears short-term sources."} {"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."} diff --git a/state/map.yaml b/state/map.yaml index 641bdc6..082c49e 100644 --- a/state/map.yaml +++ b/state/map.yaml @@ -67,7 +67,7 @@ accepted_design: - Bot, worker, providers, and RAG remain the main structural seams. - State storage stays intentionally split: Postgres for operational state, Qdrant for vectors, `.voidbot/` for archives, artifacts, logs, and backup material. - Private Void self-state lives behind one polymorphic CultCache-backed `.cc` authority with typed document kinds for self profile, moderation cursor/open obligations, speech receipts, thought memory/incubation, scheduled runtime, and candidate interventions. Do not mistake typed document ownership for separate physical stores. - - Agents and scheduled runners should cross the private-state boundary with small typed mutation operations such as cursor updates, open-case upserts, receipt records, short-term memory records, sleep-owned distillations, incubation merges, and candidate-intervention changes. + - Agents and scheduled runners should cross the private-state boundary with small typed mutation operations such as cursor updates, open-case upserts, receipt records, short-term memory records, sleep-owned distillations, incubation merges, agency-pressure changes, and candidate-intervention changes. - JSON may be used for operation payloads or debug exports, but not as an editable working projection of canonical self-state. - Deterministic cleanup code should not become a permanent organ just because recent manual cleanup requests accidentally produced compensators. The desired architecture prevents bad state from entering, then uses narrow migrations for historical debris. - `owner_codex` and `local_llm` remain distinct lanes with different tool-execution and safety posture. @@ -138,6 +138,8 @@ landed_baseline: - 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. - 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. - 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. @@ -156,5 +158,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. The sleep-maintenance model branch has a fixture smoke that proves short-term memory is consumed into durable meaning-preserving memory, and typed mood drift is reinstalled as a scheduled task. - next_action: Build a model-branch fixture for typed moderation rumination before re-enabling its scheduled task, then rebuild agency through typed operations instead of legacy compensators. + 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.