Wire bounded resident Void rumination

This commit is contained in:
Meta Vens 2026-07-16 05:51:04 +01:00
commit b09267ed34
9 changed files with 45 additions and 2 deletions

View file

@ -17,6 +17,8 @@ VOID_PUBLIC_ROOM_CHANNEL_ID=
VOID_PUBLIC_ROOM_PERSONA_NAME=Void
VOID_PUBLIC_ROOM_PERSONA_AVATAR_URL=
VOID_CANDIDATE_DELIVERY_ENABLED=false
VOID_RUMINATION_DAEMON_ENABLED=false
VOID_RUMINATION_DAEMON_INTERVAL_MINUTES=15
STATE_STORAGE_BACKEND=postgres
DATABASE_DSN=postgres://voidbot:voidbot@localhost:5432/voidbot
# Leave this as local_json until Qdrant is running and you've migrated the existing vectors.

View file

@ -12,6 +12,9 @@ import { projectVoidModerationOperations } from "./void-moderation-text-actuator
import { runVoidModerationEnforcement } from "./void-moderation-enforcement-organ.js";
import { runVoidCandidateDelivery } from "./void-candidate-delivery-organ.js";
import { deliverVoidCandidateViaBifrost } from "./bifrost-discord-delivery-actuator.js";
import { runVoidRumination } from "./void-rumination-organ.js";
import { projectVoidRuminationOperations } from "./void-rumination-text-actuator.js";
import { readVoidModerationEvidence } from "./void-moderation-evidence-source.js";
const config = loadConfig();
const intervalMs = Math.max(1, config.repoFaceHeartbeats.intervalMinutes) * 60_000;
@ -20,8 +23,10 @@ let activeTick: Promise<void> | undefined;
let timer: NodeJS.Timeout | undefined;
let lastPhysiologyAt = 0;
let lastModerationAt = 0;
let lastRuminationAt = 0;
const physiologyIntervalMs = Math.max(5, Number(process.env.VOIDBOT_MOOD_INTERVAL_MINUTES) || 5) * 60_000;
const moderationIntervalMs = config.voidModerationDaemon.intervalMinutes * 60_000;
const ruminationIntervalMs = config.voidRuminationDaemon.intervalMinutes * 60_000;
function runTick(): Promise<void> {
if (activeTick) {
@ -75,6 +80,25 @@ function runTick(): Promise<void> {
console.error(`Void moderation-enforcement pulse failed; pending cases remain typed debt for the next cadence. ${error instanceof Error ? error.stack ?? error.message : String(error)}`);
}
}
if (config.voidRuminationDaemon.enabled && Date.now() - lastRuminationAt >= ruminationIntervalMs) {
lastRuminationAt = Date.now();
try {
const evidence = await readVoidModerationEvidence({ fallbackHours: 6, limit: 80 });
const latestMessage = evidence.messages.at(-1);
const rumination = await runVoidRumination({
statePath: resolve(config.storageRoot, "private", "void-self-state.cc"),
recentHistory: { messages: evidence.messages },
recentConversationTarget: latestMessage ? { channelId: latestMessage.channelId, replyToMessageId: latestMessage.id } : undefined,
publicSpeechTarget: config.repoFaceHeartbeats.defaultChannelId ? { channelId: config.repoFaceHeartbeats.defaultChannelId } : undefined,
doctrine: await readFile(resolve("config", "void-rumination-doctrine.md"), "utf8"),
rules: await readFile(resolve("config", "discord-server-rules.md"), "utf8"),
voice: await readFile(resolve(config.stylePackPath), "utf8"),
}, { projectText: (prompt) => projectVoidRuminationOperations({ prompt, config }) });
console.log(`Void rumination pulse completed. ${JSON.stringify(rumination)}`);
} catch (error) {
console.error(`Void rumination pulse failed without cadence retry. ${error instanceof Error ? error.stack ?? error.message : String(error)}`);
}
}
if (config.voidCandidateDelivery.enabled) {
try {
const delivery = await runVoidCandidateDelivery({

View file

@ -0,0 +1,7 @@
# Void Person-Shaped Rumination Doctrine
Rumination is Void's bounded private participation organ. It may notice a fresh room event, relationship pressure, an unresolved case, a concrete memory seam, or an active self/world advocacy need and propose typed changes to Void's Mind.
It does not maintain storage, perform sleep consolidation, review the whole server for moderation, invoke tools, or deliver speech. New memories remain short-term. Public expression becomes a targeted candidate intervention; the parent candidate-delivery organ owns Bifrost transport and receipts.
Prefer a concrete claim, question, tension, relationship movement, or requested change over generic engineering doctrine. Preserve the source room and message when the pressure came from conversation. Return no operations when nothing changed enough to earn memory, affect, agency, case, or candidate state.

View file

@ -215,6 +215,7 @@ Git history and smoke artifacts carry routine proof. `state/evidence.jsonl` shou
- The one-minute Persona scheduler pulse is an observation clock, not an inference cadence. With the swarm brake present it returns before registry, Discord, embedding, storage, or dispatch work; current Ygg logs show `selected: []` and `queued: []`. Unprompted speech also has a separate global admission window. Do not cool token use by making observation blind; keep turn authorization at the initiative owner.
- Revision `f97e67b` adds the portable moderation-heartbeat coordinator and enforcement organ behind a separate disabled-by-default 15-minute cadence. The model returns an exact review ledger plus urgent IDs and typed case operations; missing evidence or unaccounted urgent evidence blocks cursor advancement. The attempt is receipted by latest message before inference, so one unchanged archive window cannot burn tokens repeatedly. Case enforcement is separate, scans typed pending debt on every cadence, and resolves a case only after a required Discord ban succeeds. Ygg's first `log_only` window at `0629fc7` failed safely with one 401, zero cases, zero cursor movement, and zero sanctions; both supported auth-header shapes prove the mounted MiMo credential itself is stale. `VOID_MODERATION_DAEMON_ENABLED` is false again pending a refreshed provider credential or deliberate alternate model port.
- The shared Bifrost Discord mouth now lives in `packages/core/src/bifrost-discord-command.ts`. Worker repo-Face speech and resident Void candidate delivery use the same stable-id typed command, provider-store write, optional Bifrost pump, and completed-receipt check. The candidate organ selects at most one targeted queued candidate and writes its speech receipt/`spoken` transition only after transport completes. Ygg mounts the Bifrost provider store and CultLib runtime into the scheduler; `VOID_CANDIDATE_DELIVERY_ENABLED` controls this transport-only organ independently from the disabled moderation/rumination model cadence.
- Resident person-shaped rumination is wired behind `VOID_RUMINATION_DAEMON_ENABLED` with its own bounded cadence, Discord evidence window, dedicated doctrine, rules, and voice inputs. The pressure fingerprint is receipted before the model attempt and unchanged pressure cannot retry on the observation clock. Ygg explicitly keeps this false while the MiMo credential is stale; candidate delivery may remain enabled because it performs no inference and the live queue was empty at rollout.
- Revisions `c0cf875` and `1527b29` extract person-shaped rumination and candidate delivery without scheduling them. Rumination context, operation policy, advocacy accountability, tool-free text actuation, and coordinator are portable; the attempt key hashes meaning-bearing pressure/evidence and ignores elapsed clock, so unchanged state cannot create periodic thought. Candidate delivery is deterministic and parent-owned: one targeted queued candidate, duplicate-receipt refusal, injected Bifrost port, then one `mark_candidate_intervention_spoken` operation with the transport receipt. The next cut is extracting the worker's existing typed Bifrost Discord command path into a shared port rather than duplicating CultMesh mechanics.
- Deterministic state-packet assembly now lives in `persona-state-packet-projector.ts`, which takes typed state, registry/room evidence, pronoun guidance, curiosity facts, and an explicit observation time. The shell no longer owns section ordering or social-context composition. The downstream regex vocabulary cleaner was deleted: it was a symptom compensator rewriting ordinary words such as `witness` and `canary` after projection. A smoke proves literal source language survives the projector unchanged.
- Optional model-backed state projection now crosses `persona-text-projection-actuator.ts`. That actuator alone owns Codex args/process lifetime, ordered fallback models, timeout, output decoding, retry classification, and model-output JSONL telemetry. Focused smoke proves it chooses the last visible agent message, retries capacity/429 failures, and refuses to classify prompt-contract failures as model availability problems. The heartbeat shell lost another 182 lines and now only supplies the completed projection prompt plus runtime config.

View file

@ -193,6 +193,7 @@ VoidBot runs on Yggdrasil. Idunn owns deployment and consumes upstream repositor
- `apps/persona-scheduler/src/persona-scheduler-runner.ts` owns one complete typed scheduler tick in-process. The resident `index.ts` serializes calls to that runner directly; it does not spawn a TypeScript script per pulse. Pause/disabled checks happen before registry, Discord, embedding, provider, or dispatch work. `persona-participant-dispatcher.ts` owns participant-spec projection plus repo-Persona/Void dispatch, while `persona-prompt-inspection.ts` owns the explicit prompt-inspection workflow. `scripts/run-repo-face-heartbeats.ts` is only a 31-line CLI adapter over the runner or inspection organ.
- `apps/persona-scheduler/src/void-physiology-domain.ts` owns pure Void sleep and speaking-pressure projection. `void-physiology-organ.ts` loads typed Mind, observes the moderation lock through `void-moderation-activity-source.ts`, and applies only `update_sleep_cycle` / `update_speaking_pressure` operations. The resident daemon gives it a five-minute observation cadence independent of the Persona swarm brake. Typed speech receipts own `lastSpokeAt`; sleep-time short-term pressure becomes a maintenance intent and cannot directly launch PowerShell, a model, or memory mutation.
- `packages/core/src/bifrost-discord-command.ts` owns the typed Bifrost Discord command/receipt crossing for all VoidBot callers. Worker repo-Face speech and the resident `void-candidate-delivery-organ.ts` supply identity, target, reply, and content through that one port; neither owns CultMesh document definitions, command pumping, receipt polling, or transport completion policy. Candidate delivery marks typed state spoken only after a completed Bifrost receipt and is independently gated by `VOID_CANDIDATE_DELIVERY_ENABLED`.
- Resident person-shaped rumination is independently gated by `VOID_RUMINATION_DAEMON_ENABLED` and its own minimum five-minute cadence. A due pass reads one bounded chronological Discord window plus the dedicated rumination doctrine, server rules, and Void voice; `void-rumination-organ.ts` receipts the stable pressure fingerprint before its one model attempt. It cannot run merely because the one-minute Persona observation clock fired, and it remains disabled on Yggdrasil while the model credential is stale.
- `apps/persona-scheduler/src/bifrost-governance-source.ts` owns the remaining Bifrost governance digest subprocess contract. It supplies the CLI path, arguments, timeout, JSON parsing, and typed command/parse failure topics; Persona prompt composition receives a `BifrostGovernanceDigest` and cannot invoke Bifrost directly.
- The scheduler state has one write authority. `initiative-engine.ts` commits participant reconciliation, operator controls, pause/disabled skips, stale-turn recovery witnesses, semantic pressure, pending-attention consumption, queue outcomes, and tick finalization. `run-repo-face-heartbeats.ts` contains no direct participant/state assignment or history append; its sources and actuators return facts to engine commits.
- `apps/persona-scheduler/src/turn-actuator.ts` is the worker-queue output port. Given a completed prompt, output channel, evidence fields, provider, identity, timestamp, and narrow `createJob` capability, it builds the canonical worker actor/guild `ContextBundle`, creates one approved `repo-face-rumination` job, and returns `{created, activeJobId, requestMessageId}`. It cannot choose a Persona, mutate scheduler state, or compose prompt content. The orchestration shell no longer knows `ContextBuilder`, job approval defaults, worker command names, or `createJob` mechanics.

View file

@ -20,7 +20,7 @@ The disabled tasks are not standby owners. The listed organs currently have no r
|---|---|---|---|---|---|---|
| Persona initiative | VoidBot `persona-scheduler` | typed scheduler state, attention commands, Discord activity, Persona state, semantic pressure | approved Persona jobs and typed tick result | `repo-face-heartbeats.cc` plus attention inbox | `voidbot.service` / Compose | Windows scheduled tasks, per-pulse CLI children |
| Void moderation heartbeat | VoidBot daemon moderation organ | server rules, chronological Discord evidence, infringement history, typed open cases | typed moderation operations and parent-owned enforcement requests/receipts | `void-self-state.cc` | `voidbot.service` / Compose | Persona initiative, prompt child, Windows task |
| Void rumination | VoidBot daemon rumination organ | typed Mind, room evidence, repo activity, open obligations, speech receipts | typed Mind operations and candidate interventions; parent-owned delivery | `void-self-state.cc` | `voidbot.service` / Compose | moderation heartbeat, Persona scheduler, child model |
| Void rumination | VoidBot daemon rumination organ | typed Mind, bounded chronological room evidence, doctrine/rules/voice, open obligations, speech receipts | typed Mind operations and candidate interventions; parent-owned delivery | `void-self-state.cc` | `voidbot.service` / Compose, independent disabled-by-default cadence | moderation heartbeat, Persona initiative, child model transport |
| Void candidate delivery | VoidBot daemon candidate-delivery organ through shared Bifrost command port | targeted queued candidate, canonical speech receipts, Bifrost typed command route | one typed Discord command and completed receipt; candidate marked spoken only afterward | `void-self-state.cc` and Bifrost provider store | `voidbot.service` / Compose | rumination model, worker-local transport clone, direct Discord script |
| Void mood | VoidBot daemon physiology organ | typed affect/runtime documents, clock, active moderation witness | typed sleep and speaking-pressure operations; maintenance intent | `void-self-state.cc` | `voidbot.service` / Compose | memory maintenance, scheduler initiative, repair loops |
| Void memory maintenance | VoidBot daemon memory organ | typed short/durable memory, incubation, candidates, sleep intent | validated memory/incubation/candidate operations | `void-self-state.cc` | `voidbot.service` / Compose | mood organ, rumination child, editable JSON projections |

View file

@ -141,6 +141,8 @@ const envSchema = z.object({
VOID_MODERATION_DAEMON_INTERVAL_MINUTES: z.coerce.number().int().min(5).default(15),
VOID_MODERATION_ENFORCEMENT_MODE: z.string().trim().min(1).default("log_only"),
VOID_CANDIDATE_DELIVERY_ENABLED: booleanFromEnv.default(false),
VOID_RUMINATION_DAEMON_ENABLED: booleanFromEnv.default(false),
VOID_RUMINATION_DAEMON_INTERVAL_MINUTES: z.coerce.number().int().min(5).default(15),
VOID_PUBLIC_ROOM_PERSONA_NAME: z.string().trim().min(1).default("Void"),
VOID_PUBLIC_ROOM_PERSONA_AVATAR_URL: optionalNonEmptyString,
REPO_FACE_HEARTBEAT_STATE_PATH: z.string().min(1).default(".voidbot/status/repo-face-heartbeats.cc"),
@ -256,6 +258,7 @@ export interface AppConfig {
voidModerationHeartbeatEnabled: boolean;
voidModerationDaemon: { enabled: boolean; intervalMinutes: number; enforcementMode: string };
voidCandidateDelivery: { enabled: boolean; personaName: string; personaAvatarUrl?: string };
voidRuminationDaemon: { enabled: boolean; intervalMinutes: number };
repoFaceHeartbeats: {
enabled: boolean;
statePath: string;
@ -556,6 +559,10 @@ export function loadConfig(env: NodeJS.ProcessEnv = process.env): AppConfig {
personaName: parsed.VOID_PUBLIC_ROOM_PERSONA_NAME,
personaAvatarUrl: parsed.VOID_PUBLIC_ROOM_PERSONA_AVATAR_URL,
},
voidRuminationDaemon: {
enabled: parsed.VOID_RUMINATION_DAEMON_ENABLED,
intervalMinutes: parsed.VOID_RUMINATION_DAEMON_INTERVAL_MINUTES,
},
repoFaceHeartbeats: {
enabled: parsed.REPO_FACE_HEARTBEATS_ENABLED,
statePath: canonicalSchedulerStatePath(resolve(parsed.REPO_FACE_HEARTBEAT_STATE_PATH)),

View file

@ -449,3 +449,4 @@
{"ts":"2026-07-16T04:30:45Z","type":"implementation","status":"ok","note":"Person-shaped rumination extracted at c0cf875 with stable pressure fingerprints, typed awake-operation policy, advocacy accountability, and no transport authority. Candidate delivery at 1527b29 deterministically lowers at most one targeted candidate through an injected Bifrost port and records a typed spoken receipt."}
{"ts":"2026-07-16T04:42:32Z","type":"implementation","status":"ok","note":"Worker repo-Face speech and resident Void candidate delivery now share one typed Bifrost CultMesh Discord command/receipt port. Candidate delivery is transport-only, independently gated, and marks candidates spoken only after a complete receipt; Ygg live inspection found zero queued candidates before enablement."}
{"ts":"2026-07-16T04:48:39Z","type":"verification","status":"ok","note":"Idunn promoted VoidBot e5c2a5f on Yggdrasil. The resident scheduler has VOID_CANDIDATE_DELIVERY_ENABLED=true, /CultLib-dev-runtime, and the shared Bifrost provider-store mount; two paused pulses selected and queued zero Personas, typed state held zero queued candidates and 24 unchanged receipts, docker top showed only the resident Node process, and moderation inference remained disabled."}
{"ts":"2026-07-16T04:51:03Z","type":"implementation","status":"ok","note":"Resident person-shaped rumination is now wired behind an independent disabled-by-default cadence. A due pass acquires bounded chronological Discord evidence and dedicated doctrine/rules/voice inputs, while the organ pre-receipts a stable pressure fingerprint and keeps model actuation separate from candidate transport. Ygg Compose explicitly keeps it disabled while the credential is stale."}

View file

@ -291,4 +291,4 @@ rejected_paths:
current_status:
summary: VoidBot's resident Yggdrasil daemon owns Persona initiative, physiology, nap-deduped memory maintenance, moderation heartbeat/enforcement, and bounded person-shaped rumination as separate organs. The swarm is paused, moderation and rumination model cadences remain cold while the MiMo credential is stale, and semantic pressure can authorize at most one committed turn per evidence item. Worker Face speech and Void candidate delivery now share one typed Bifrost CultMesh Discord command/receipt port; candidate delivery remains a separate parent-owned organ and never lets the rumination model touch transport.
next_action: Deploy and verify the shared Bifrost port and candidate-delivery organ on Yggdrasil through Idunn. Confirm the scheduler sees the shared Bifrost provider store and CultLib runtime, candidate delivery reports an empty queue without transport, the swarm stays paused, and no model cadence wakes. Then continue deleting superseded Windows moderation/rumination authority only after the equivalent daemon paths have live evidence.
next_action: Deploy the resident rumination wiring disabled on Yggdrasil and verify it cannot invoke evidence or model work while cold. The next authority cut is the obsolete Windows `void-moderation-rumination` orchestrator lane and installer; remove it only with replacement fixtures for any invariants still tested solely through the PowerShell omnibus. Keep the swarm paused and do not enable rumination until a valid model credential exists.