From 04dc8e330390cfce756b89804a53dcfb1dc9eed0 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 16 Jul 2026 06:03:14 +0100 Subject: [PATCH] Delete local Void mood and memory runners --- .env.example | 4 - README.md | 3 +- apps/bot/src/discord-bot.ts | 41 +- notes/fresh-workspace-handoff.md | 9 +- notes/voidbot-current-system-map.md | 21 +- package.json | 5 - prompts/void-memory-maintenance.md | 122 --- scripts/install-void-mood-drift-task.ps1 | 89 --- .../void-rumination-context-projection.ps1 | 744 ------------------ scripts/run-void-memory-maintenance.ps1 | 660 ---------------- scripts/run-void-mood-drift.ps1 | 70 -- scripts/simulate-void-mood.mjs | 400 ---------- .../smoke-void-agency-pressure-fixture.ps1 | 85 -- ...-void-memory-maintenance-sleep-fixture.ps1 | 183 ----- state/evidence.jsonl | 1 + state/map.yaml | 18 +- 16 files changed, 47 insertions(+), 2408 deletions(-) delete mode 100644 prompts/void-memory-maintenance.md delete mode 100644 scripts/install-void-mood-drift-task.ps1 delete mode 100644 scripts/lib/void-rumination-context-projection.ps1 delete mode 100644 scripts/run-void-memory-maintenance.ps1 delete mode 100644 scripts/run-void-mood-drift.ps1 delete mode 100644 scripts/simulate-void-mood.mjs delete mode 100644 scripts/smoke-void-agency-pressure-fixture.ps1 delete mode 100644 scripts/smoke-void-memory-maintenance-sleep-fixture.ps1 diff --git a/.env.example b/.env.example index 0920f8b..49f68d2 100644 --- a/.env.example +++ b/.env.example @@ -137,8 +137,4 @@ VOIDBOT_HEALTHCHECK_NOTIFY_REPEAT_HOURS=12 VOIDBOT_STACK_STARTUP_TASK_NAME=VoidBot Stack Startup VOIDBOT_HEALTHCHECK_TASK_NAME=VoidBot Operations Watchdog VOIDBOT_HEALTHCHECK_INTERVAL_MINUTES=60 -VOIDBOT_MODERATION_TASK_NAME=Void Moderator Rumination -VOIDBOT_MODERATION_INTERVAL_MINUTES=15 -VOIDBOT_MODERATION_HEARTBEAT_INTERVAL_MINUTES=1 VOID_MODERATION_ENFORCEMENT_MODE=policy -VOID_MODERATION_TIMEOUT_MINUTES=60 diff --git a/README.md b/README.md index bc826ef..c2601d3 100644 --- a/README.md +++ b/README.md @@ -572,7 +572,6 @@ Tracked prompt surfaces: - `config/discord-server-rules.md` - `config/moderation-review-agent.md` - `config/void-rumination-doctrine.md` -- `prompts/void-memory-maintenance.md` - `styles/void-default.md` Local moderation-state surfaces: @@ -624,7 +623,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. +Resident physiology owns sleep transitions and emits a typed maintenance intent. The memory-maintenance organ pre-receipts one attempt per nap, runs one bounded tool-free model call when pressure exists, validates the complete operation batch, and fails unless short-term residue is consumed. The consolidated resident-organ smoke proves nap intent, operation boundaries, and residue verification without a PowerShell runner. 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. diff --git a/apps/bot/src/discord-bot.ts b/apps/bot/src/discord-bot.ts index bd84fe2..6b78d67 100644 --- a/apps/bot/src/discord-bot.ts +++ b/apps/bot/src/discord-bot.ts @@ -22,7 +22,6 @@ import { findRepoDiscordIdentityByTextAddress, isRepoDiscordIdentityAllowedInChannel, loadFaceIdentityRegistry, - queueAgentHeartbeatMention, queueRepoFaceMention, type RepoDiscordIdentity, stripRepoIdentityTextAddress, @@ -64,8 +63,10 @@ import { maybeMirrorOwnerAquariumMessageAsMetameVoice } from "./metame-owner-voi import { startRepoFaceVoicePlayback } from "./repo-face-voice-playback"; import { buildActorFromInteraction, + buildActorFromMessage, buildChannelIndexingTarget, buildGuildContextFromInteraction, + buildGuildContextFromMessage, filterPromptEchoHistoryResults, formatArchivedMessageContext, formatHistoryResults, @@ -73,6 +74,7 @@ import { formatSourceResults, getRecentMessages, getRoleIdsFromInteraction, + getRoleIdsFromMessage, ingestIfIndexed, materializeMessage, notifyOwnerOfBotIssue, @@ -453,19 +455,32 @@ export async function startBot(): Promise { return; } - const queuedMention = await queueAgentHeartbeatMention({ - statePath: config.repoFaceHeartbeats.statePath, - identityId: "void", - channelId: message.channelId, - messageId: message.id, - authorId: message.author.id, - authorName: message.author.username, - content: message.content, - visiblePrompt: visiblePrompt || "Void was mentioned without a visible prompt; inspect recent room context and decide whether a response is warranted.", + await handlePrompt({ + prompt: visiblePrompt || "Void was mentioned without a visible prompt; inspect recent room context and decide whether a response is warranted.", + command: "ask", + actor: buildActorFromMessage(message), + roleIds: getRoleIdsFromMessage(message), + guildContext: buildGuildContextFromMessage(message), + outputChannelId: message.channelId, + requestMessageId: message.id, + channel: message.channel.isTextBased() ? message.channel : null, + respond: async (content) => { await message.reply(content); }, + config, + permissionEngine, + contextBuilder, + retrievalService, + archiveRepository, + sourceArchiveRepository, + jobQueue, + auditLog, + interactionMemory, + voidUsageRateLimiter, + providerRegistry, + situationalSocialReadInferer, + stylePack: activeStylePack, + systemMessages: activeSystemMessages, }); - console.log( - `Queued Void mention ${message.id} via CTB turn queue (${queuedMention.pendingCount} pending).`, - ); + console.log(`Routed Void mention ${message.id} through the normal bot prompt lane.`); } catch (error) { console.error(error); await notifyOwnerOfBotIssue( diff --git a/notes/fresh-workspace-handoff.md b/notes/fresh-workspace-handoff.md index 1bff14a..b67dccb 100644 --- a/notes/fresh-workspace-handoff.md +++ b/notes/fresh-workspace-handoff.md @@ -37,8 +37,7 @@ Git history and smoke artifacts carry routine proof. `state/evidence.jsonl` shou - `scripts/export-recent-repo-activity.mjs` reads repo cursors from the typed `.cc` store. Legacy `.json` cursor arguments are only mapped to a sibling `.cc` path when needed by old invocation surfaces. - The Windows rumination runner, installer, CTB moderation launcher, and PowerShell-only rumination/moderation fixtures are deleted. Their live invariants now belong to the resident TypeScript moderation, rumination, enforcement, and candidate-delivery organs and the consolidated initiative-engine smoke. - 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 anchor. +- Resident physiology owns typed sleep/speaking pressure and emits maintenance intent. Resident memory maintenance consumes that intent once per nap, validates a whole operation batch, and verifies short-term residue is gone; the consolidated TypeScript smoke covers this without a local mood or memory runner. - Persona scheduling is no longer part of the hidden Windows pulse. Yggdrasil runs `apps/persona-scheduler` as a resident compose service inside `voidbot.service`; it serializes CTB ticks and leaves execution to the worker. `GameCult Local Orchestrator` still describes legacy/local ownership for other organs and must not be treated as the Persona owner. - `scripts/start-voidbot-stack.ps1` now treats Bifrost's CultMesh command lane as a startup dependency. It reads `BIFROST_ROOT`, `BIFROST_CULTMESH_COMMAND_URI`, and `BIFROST_CULTMESH_STORE_PATH`, exports the provider store when missing, verifies the command processor exists, and records the result under `runtime-stack.json` as `bifrost`. - r/GameCultOrg is the GameCult subreddit. Personas can now post there through @@ -52,11 +51,11 @@ Git history and smoke artifacts carry routine proof. `state/evidence.jsonl` shou complete as the repo Face path. - `npm run moderation:rumination:fixture` proves the non-skip typed rumination branch without spending a live model call: it seeds a temporary `.cc`, routes the runner through a fake Codex child, and verifies short-term memory, incubation, agency pressure, and candidate intervention proposals are parent-applied typed operations with no exact timestamp leakage in the prompt-facing context. - `npm run moderation:rumination:speech-fixture` proves queued speech has a parent-owned action loop: fake Codex queues one candidate with a delivery target, fake Discord send writes a receipt, and the parent runner marks the candidate spoken through typed state. The child prompt now forbids direct send-script calls. -- The first live sleep-maintenance gate passed on real state. A manual typed nap ran `scripts/run-void-memory-maintenance.ps1`, consumed the scheduled rumination short-term memory with one `apply_memory_distillation` operation, and left `thoughtMemory.shortTerm` empty while preserving the durable identity seam with target, claim, tension, action implication, and anchors. +- The first historical live sleep-maintenance gate consumed scheduled-rumination residue into a meaning-preserving durable identity seam. The manual runner used for that proof is now deleted; current ownership is the resident nap-deduped memory organ. - A later live-loop inspection found one stale answered-case summary, four stale short-term memories, and an unbounded rumination/memory-maintenance failure mode. The current hardened path now verifies candidate delivery postconditions, normalizes answered-case wording on receipt/closure, uses process-aware locks plus Codex timeouts, and a fresh memory-maintenance pass reduced live short-term memory back to zero. - `npm run moderation:rumination:nap-skip-fixture` proves awake rumination does not keep making fresh short-term residue during sleep: when the typed sleep cycle is napping and there are no new room messages or open cases, the runner writes `[]` and exits before invoking Codex. - The old memory-organ script family, legacy moderation-state template, and legacy moderation context exporter have been deleted. Do not revive their cleanup, agency, dream, vector-incubation, or mirror-repair behavior as compatibility scaffolding. -- `scripts/run-void-memory-maintenance.ps1` is the first replacement memory-maintenance organ. It builds a bounded context from typed memories/incubation/candidates/runtime, loads `prompts/void-memory-maintenance.md`, and applies only allowed memory/incubation/candidate typed operation proposals through `scripts/void-self-state.mjs`. In sleep mode, memory pressure sets `sleepDirective.forceDistillation`; a real model pass returning no operations under pressure fails visibly. +- The PowerShell mood and memory-maintenance bodies, installers, shared chronology helper, and their shell-only fixtures are deleted. Their state transitions and gates exist only in resident TypeScript organs. - Rumination does not write durable memory. It can only emit `record_short_term_memory`; sleep maintenance must promote short-term records with `apply_memory_distillation`, merge them into incubation and prune them, or prune them outright. A real sleep pass that leaves short-term records behind fails. - Short-term rumination memory is clustered at the typed service boundary now. When a new `record_short_term_memory` operation repeats the same target/topic pressure, the service keeps one provisional memory, merges anchors/tags, preserves a stable source id for sleep, and updates the thought instead of stacking duplicates. This is meant to protect deepening, not suppress it: low-saturation or newly anchored thoughts should keep developing inside the cluster until they stop changing. `npm run moderation:short-term-clustering:fixture` proves the guard. - Long-term memory is durable, not immutable. `revise_durable_memory`, `retire_durable_memory`, and `crystallize_memory_into_identity` make memory reconsolidation explicit; `npm run moderation:memory-lifecycle:fixture` proves a short-term thought can promote, revise, retire superseded durable records, and crystallize into an identity memory plus self-profile value. @@ -66,7 +65,7 @@ Git history and smoke artifacts carry routine proof. `state/evidence.jsonl` shou - Urgent safety handling can now enforce through a parent-owned Discord action path. `VOID_MODERATION_ENFORCEMENT_MODE=policy` enables instant bans and three-strike bans according to the explicit table in `config/discord-server-rules.md`; the child only opens typed cases with `infringement:` plus one moderation disposition tag. The child prompt still does not own raw sanction authority. - Deferred candidates are not delivered speech and no longer satisfy active advocacy pressure by themselves. The runner now delivers existing queued candidates with delivery targets, not only candidates proposed by the current model pass. The first live repair used Metacrat's explicit channel permission to move the stranded AquariumSynthHost candidate from deferred/no-target to queued/channel-target, then delivered it through the normal bot transport and recorded a typed speech receipt. - Parent-owned delivery now also checks canonical typed speech receipts before using the mouth. If a queued candidate targets a Discord reply that already has a receipt, the runner retires it instead of answering the same message twice in slightly different prose. Typed state writes also normalize historical receipts to keep the earliest canonical reply per `channelId + replyToMessageId`. `scripts/smoke-void-rumination-speech-fixture.ps1` now seeds a prior receipt and proves both same-pass sibling retirement and already-answered target retirement. -- Speaking pressure is allowed to refresh while moderation is active. `scripts/simulate-void-mood.mjs` still skips sleep-cycle mutation and memory maintenance under a recent moderation lock, but it now reloads typed state and applies `update_speaking_pressure` first, so fresh canonical speech receipts update `lastSpokeAt`, damping, and `needToSpeak` even during long rumination runs. +- The resident daemon serializes organ work, so physiology no longer infers mutual exclusion from a stale filesystem lock. Fresh canonical speech receipts directly update `lastSpokeAt`, damping, and `needToSpeak` on the physiology cadence. - 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, prompt contract, fixture coverage, and one live-state pass. 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. diff --git a/notes/voidbot-current-system-map.md b/notes/voidbot-current-system-map.md index c5d1e7e..7f397ea 100644 --- a/notes/voidbot-current-system-map.md +++ b/notes/voidbot-current-system-map.md @@ -121,21 +121,14 @@ VoidBot runs on Yggdrasil. Idunn owns deployment and consumes upstream repositor - installs the hidden logon task that runs `start-voidbot-stack.ps1` automatically after reboot or sign-in. - `scripts/check-voidbot-operations.ps1` - watchdog for process liveness, Qdrant, Postgres, Ollama, source-repo reconcile drift, Discord auth, backup freshness, offsite sync freshness, and optional ignored local extension checks. -- `scripts/lib/void-rumination-context-projection.ps1` - - remaining compatibility projector for the manual PowerShell memory-maintenance body. Resident moderation, rumination, and memory organs use the TypeScript relative-chronology projector. - `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, 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 bounded 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. - - Both rumination runners use process-aware locks. Live locks inside the runtime window skip; dead locks can be reclaimed; live over-runtime locks fail closed instead of allowing overlapping mutation. Each runner writes `starting` status immediately after claiming its own lock. - `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-short-term-clustering-fixture.ps1` - short-term clustering fixture. It proves repeated same-target/topic rumination proposals fold into one provisional memory with merged anchors, while a different topic in the same repo stays separate. -- `scripts/smoke-void-agency-pressure-fixture.ps1` - - agency-pressure fixture. It seeds one typed self-advocacy pressure, verifies it appears in the rendered self-state summary, then runs mood drift against an isolated fixture state to prove agency pressure contributes to speaking pressure without creating a hard-wired speech candidate. - `scripts/smoke-persona-initiative-engine.ts` - consolidated resident-organ fixture. It proves moderation review/enforcement boundaries, rumination fingerprint and nap gates, typed advocacy accountability, candidate selection, shared Bifrost command receipts, and no-double-delivery without summoning a Windows runner. - `scripts/run-gamecult-orchestrator.ps1` @@ -146,8 +139,6 @@ VoidBot runs on Yggdrasil. Idunn owns deployment and consumes upstream repositor - passed once on real state after a manual typed nap. The maintenance runner consumed the scheduled rumination short-term memory with one `apply_memory_distillation` operation, leaving short-term memory empty and preserving one durable identity seam with target, claim, tension, action implication, and anchors. - Sleep brake - scheduled rumination skips during naps when there are no new room messages and no open cases, so sleep can distill the short-term surface without awake rumination immediately adding fresh residue. -- `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. 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. - Repo Discord identities - `REPO_DISCORD_IDENTITIES_PATH` points at a private JSON registry of repo id, repo name, display name, optional avatar URL, optional role id, and allowed channel ids. The role is the Discord mention target; the webhook persona is the speech transport. - That registry may also carry `channelPermissions`: per-topic channel entries with a speech threshold and speed multiplier. These are not another transport; they are the permission/salience map for where a Face may speak. Aquarium is the low-threshold musing lane, while domain channels such as narrative, gamedesign, music, soundtrack, development, programming, visual-effects, machine-learning, and unity3d are available to matching Faces. @@ -162,7 +153,7 @@ VoidBot runs on Yggdrasil. Idunn owns deployment and consumes upstream repositor - Repo Faces are allowed to build a long-running map of their repo and propose concrete changes, including lore/design repairs. Discord jobs remain read-only: a Face must ask for human consensus before implementation changes canonical repo material. Once a proposal is concrete enough to review, it should cross the parent-owned proposal PR rail so GitHub becomes the durable argument surface. If a Face state grants a bylined essay lane, authored opinion essays can carry that Face's own vision without canon consensus as long as the authorship is explicit. - On first role-addressed chat, `ensureRepoFaceInitialized` creates the target repo's local `.voidbot/voice`, `.voidbot/state`, `.voidbot/birth`, and `.voidbot/logs` folders, writes `voice/identity.json`, and launches `scripts/run-repo-face-birth.mjs` detached if no birth summary is already present. - `scripts/run-repo-face-birth.mjs` is the observable bridge into EpiphanyAgent's newborn path. It runs `epiphany-repo-personality scout --root ` to gather terrain/history, then runs `epiphany-repo-birth-runner` with repo-local state stores. The default mode is `plan`, so first chat creates reviewable birth artifacts without auto-accepting hidden personality mutations. - - Role mentions, whole-message display-name mentions, and human replies to a Face's webhook/persona message append pending Face obligations to `pendingMentions` in `REPO_FACE_HEARTBEAT_STATE_PATH`; ordinary bot mentions and DMs append pending obligations for the built-in `void` participant. They no longer create direct provider jobs from `MessageCreate`. Name matching is exact-token matching across the message, not only prefix matching, so a human introduction such as "Meet Kiko, Huginn and Weksa" queues all named Faces. Mention detection may happen outside regular autonomous Face chatter lanes such as general; speech still crosses the normal registered channel/action rails, and the scheduler attaches the source channel snapshot so the Face can see where the call came from. The CTB scheduler temporarily prioritizes agents with pending mentions, attaches those prompts plus reply targets to the next `repo-face-rumination` or Void moderation pass, and consumes the pending items only after a turn is actually queued. Rumination jobs remain private unless they explicitly emit a parent-owned Face action block or a typed candidate intervention; the worker must never auto-post their summaries as the base bot. Heartbeat prompts now include the channel permission plan, recent context from non-primary permitted channels, social embodiment, jurisdiction respect, comedy/improv doctrine, and anti-template sampling guidance so a Face can jump into domain chatter, form durable social reads, consult the right steward, and answer people without copying nearby note formulas. + - Role mentions, whole-message display-name mentions, and human replies to a Face's webhook/persona message append pending Face obligations to the typed attention inbox resolved from `REPO_FACE_HEARTBEAT_STATE_PATH`. Ordinary bot mentions and DMs use the normal Void bot prompt/job lane immediately; they never enter Persona initiative or create a built-in Void participant. Name matching is exact-token matching across the message, not only prefix matching, so a human introduction such as "Meet Kiko, Huginn and Weksa" queues all named Faces. Persona speech still crosses registered channel/action rails, and the scheduler consumes pending items only after a turn is actually queued. - Repo Face proposal/article PRs, PR comments, registered-identity Discord posts, and r/GameCultOrg Reddit thread posts now cross through parent-owned action blocks before Bifrost bridge commands. Bylined essays use the Interpreter-owned `ARTICLE` block: the Face writes naturally, the Interpreter extracts structured title/description/author/date/tags/path/body fields, and deterministic worker code renders/validates YAML frontmatter before calling `BIFROST_ROOT/tools/bifrost-bridge.mjs` for a draft PR and persona announcement. Subreddit threads use the Interpreter-owned `REDDIT THREAD` block: the Face writes a public title/body/share line, and deterministic worker code calls Bifrost `reddit-post` with Persona flair. The old `VOIDBOT_REPO_IDENTITY_ARTICLE` JSON sentinel is cut. Proposal PR and PR-comment sentinels still exist as compatibility rails. This is not merely helper placement: Bifrost's original governance/labor platform role makes it the owner of governed public crossings for GameCult work, including GitHub, r/GameCultOrg Reddit transport/thread access, Discord-native work/member/swarm interactions, dispatch receipts, CultNet/CultCache intake, and future collaboration interfaces. Heimdall is the OAuth/account/capability gate; Bifrost is the bridge/request/routing/receipt authority. VoidBot still owns room/thread cognition, moderation judgment, archive/source retrieval, and Face personality. - Repo Face turns now receive a Bifrost governance digest from `BIFROST_ROOT/tools/governance-threads.mjs digest --repo --agent ` alongside recent Discord context. Bifrost typed CultCache topics/comments are the canonical home for feature requests, Face opinions, missing questions, approvals, and dispatch receipts; Discord is a mirror/evidence source. When `BIFROST_DISCORD_CHANNEL_ID` is configured for `#bifrost`, repo Face Discord context ignores mirrored bot chatter there because the authoritative activity arrives through the Bifrost digest. Human `#bifrost` messages should only become governance comments after Bifrost/Heimdall can link the Discord id to a registered actor. Repo-local asks that would send a human or Codex to add/fix/name/scaffold/document/test/investigate/implement belong on Bifrost with enough room/repo context for a claimed Codex turn; a Face may still speak characterfully in Aquarium, but not as the only dispatch surface. Legacy `UPDATE REQUEST` packets are parsed only as compatibility input and reconciled into Bifrost topics instead of immediate transport enqueue. Local `#bifrost` is channel `1506760343836430376`; work proposals, governance mirrors, and Bifrost dispatch receipts should go there, while Aquarium remains the casual/social swarm room. - Repo character turns now have a state-projector plus turn-Interpreter shape. The state-in boundary is `prompts/repo-face-state-projector.prompt.md`: typed state and identity material are translated into lived character prose before the child sees it. The state projector must surface values, drives, wounds, fascinations, aesthetic taste, dignity, mood, needs, social reads, bonds, agency pressure, and the personality-specific meaning of sparse or dense social topology when the state contains those facts. The deterministic state packet feeding that projector is intentionally verbose and parent-facing: it exposes private notes, values, activation vectors, runtime rest/speaking pressure, affect needs, bonds, status reads, mood dimensions, durable memories, short-term residue, agency pressures, incubating thoughts, candidate speech pressure, recent speech residue, social graph facts, room weather, and semantic curiosity graph attractors so every included variable can appear directly or bend behavior. The curiosity graph is built by seeding history/source vector retrieval from current room context, nearby channels, typed Face state, and home-jurisdiction identity, then decoding retrieved chunks into weighted attractor clusters with prominence, saturation, novelty, density, jurisdiction fit, suggested motion, and evidence labels. The projector owns turning those neutral facts into lived curiosity, boredom, fatigue, territorial pull, underexplored intrigue, or a closing/pivot impulse; deterministic code must not decide the feeling. Deterministic code may also attach neutral graph facts such as mapped people and unmapped registered peers to the projector packet, but it must not decide whether that feels like anxiety, territorial scouting, or nothing. Deterministic code attaches recent read-only home-repo activity before chat, using the Face's own state path as the repo-activity cursor source, so current implementation motion is visible as body context. Current-room and nearby-channel logs no longer cross a model conversation projector; deterministic code attaches raw oldest-to-newest transcript evidence with explicit recency/correction rules so the Face can notice human reframes directly. Deterministic code also attaches a social-affordance surface that matches active typed person-bonds/status reads to recent raw messages from that person, so relationship state arrives as a live hook rather than private decorative lore. Deterministic code still owns full prompt assembly and inspection through `scripts/run-repo-face-heartbeats.ts --assemble-prompt `; that path composes stable doctrine, allowed rooms, research/archive capability guidance, Bifrost digest, projected memory/affect prose, home-repo activity, live social affordances, and raw conversation evidence into the exact child prompt. It must not invent personality, mood, affect, or social memory from typed state. Situational obligations such as direct-call priority or first-public-introduction duty are emitted only when the scheduler packet proves they apply, not as standing prompt law. The child character writes a natural in-character turn on full `gpt-5.4` and may describe desired speech, repo/article proposals, social reads, needs, mood shifts, or memory-worthy thoughts without emitting action DSL blocks. The cheaper Spark/Mini parent Interpreter decides route/retry/drop and translates warranted effects into structured `STATE NOTE` and `SAY` blocks for the worker while governance dispatch remains paused. `STATE NOTE` blocks become typed short-term memory, affect needs, social bonds, status reads, mood dimensions, or agency pressure. The worker still accepts legacy compact JSON sentinels and legacy `UPDATE REQUEST` blocks for compatibility, but the live prompt path keeps transport and state syntax out of the character's roleplay surface. @@ -175,7 +166,7 @@ VoidBot runs on Yggdrasil. Idunn owns deployment and consumes upstream repositor - `persona-turn-prompt-projector.ts` is the final pure turn-policy owner. It composes the repo-Face prompt template and owns identity doctrine, situation/mention/channel/research/social/governance/repetition/worldbuilding directives, plus deterministic jurisdiction-dive cadence. The heartbeat shell passes acquired facts and sends the returned prompt to the actuator; it does not load prompt templates or decide prompt policy. - Typed native Personas and repo Personas share the same `persona-state-source`/memory-projector path. Legacy `gamecult.persona_state.v0` JSON remains live for Muninn, Sleipnir, Hermodr, and Tengu behind `persona-portable-state-source.ts` and `persona-portable-state-projector.ts`; the source validates schema/read failures and the projector renders bounded imported state without exposing its path. This compatibility organ is not canonical state ownership and should disappear after a provenance-aware `.cc` migration. - `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. +- `apps/persona-scheduler/src/void-physiology-domain.ts` owns pure Void sleep and speaking-pressure projection. `void-physiology-organ.ts` loads typed Mind and applies only `update_sleep_cycle` / `update_speaking_pressure` operations. The resident coordinator serializes organs, so physiology does not infer activity from a filesystem lock. Typed speech receipts own `lastSpokeAt`; sleep-time short-term pressure becomes a maintenance intent and cannot directly launch a model or mutate memory. - `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. @@ -189,10 +180,6 @@ VoidBot runs on Yggdrasil. Idunn owns deployment and consumes upstream repositor - The Windows `GameCult Local Orchestrator` and per-organ scheduled-task wrapper no longer own Persona pulses. `voidbot.service` deploys `persona-scheduler` beside bot, worker, Qdrant, and the CultMesh publisher. Turn frequency is controlled by `REPO_FACE_HEARTBEAT_BASE_RECOVERY_MINUTES`, `REPO_FACE_HEARTBEAT_GLOBAL_HEAT`, `REPO_FACE_HEARTBEAT_SPEED_OVERRIDES`, and `REPO_FACE_HEARTBEAT_HEAT_OVERRIDES`. Heat keys can target `all`, `kind:`, `turn:`, `identity:`, `repo:`, `display:`, `channel:`, or the bare identity/repo/display key. - The local ignored registry currently includes Nibu for `AetheriaLore`, with Face state in `E:/Projects/AetheriaLore/.voidbot/state/nibu.cc`, Discord role id `1505918221453103247`, and an Aquarium channel allow-list. Nibu's Face state now treats AetheriaLore-first map-building, abrasive character voice, consensus-backed change agency, her own `Aetheria/Lore/Nibu.md` character-note stewardship, Nibu-authored lore essays, and tangential setting fascination as separate active doctrines. That tangential fascination points at embodied ship minds, junkyard abandonment, murderous autonomy, salvage, and save-scumming survival. The repo Face heartbeat organ is now pulsed by `GameCult Local Orchestrator`; after restarting the stack onto the new worker code, forced heartbeat job `76020f1e-0c55-424b-9e35-89ba7f77fcc3` completed without base-bot auto-posting. - The local ignored registry now also includes Epiphany for `EpiphanyAgent`, with Face state in `E:/Projects/EpiphanyAgent/.voidbot/state/epiphany.cc`, Discord role id `1506375937439301642`, and Aquarium channel allow-list `1501196543150264332`. Her repo-local `.voidbot` birth surface has completed in plan/review mode; the generated EpiphanyAgent startup packets still require Self review before accepted native birth mutations. Her jurisdiction is the self-improvement spine: typed state, CultCache/CultNet, Codex bridge boundaries, birth/init rites, low-copy runtime contracts, review gates, graph-shaped memory, heartbeat physiology, and deletion of duplicated authority. -- `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` - - installs the local 5-minute scheduled task that keeps the shared self-state emotionally non-flat between moderation passes. - `scripts/export-random-discord-history.mjs` - plain Node helper for novelty excursions that samples one random archived Discord seam, returns a local context window around the anchor message, and skips bot-directed prompt rows by default. - `scripts/feed-codex-chat-consensus.mjs` @@ -307,7 +294,7 @@ Within the Postgres path, the implementation is split on purpose now too: ## Flow 6: Physiology Runtime 1. The resident `apps/persona-scheduler` daemon invokes `void-physiology-organ.ts` on its own five-minute cadence, independent of whether Persona initiative is paused. -2. The organ reads typed documents from `.voidbot/private/void-self-state.cc`, derives the most recent speech time from typed receipts, and observes the moderation lock through narrow injected ports. +2. The organ reads typed documents from `.voidbot/private/void-self-state.cc` and derives the most recent speech time from typed receipts. Resident coordinator serialization is the mutual-exclusion boundary. 3. It updates only `void.scheduled_runtime.sleepCycle` and `void.scheduled_runtime.speakingPressure` through `update_sleep_cycle` and `update_speaking_pressure` operations. Active moderation freezes sleep transitions but does not prevent pressure observation. 4. When a nap has short-term residue, physiology emits a typed memory-maintenance intent. It does not perform memory policy, launch PowerShell, or call a model. 5. The Windows omnibus mood writer has been removed. Legacy mood scripts may remain as fixtures or manual compatibility bodies, but they are not a deployed recurrence or runtime owner. @@ -321,7 +308,7 @@ Within the Postgres path, the implementation is split on purpose now too: 5. The organ parses the entire response as a JSON operation array, validates every item against the canonical self-state operation schema, and rejects operations outside the explicit maintenance family before applying any item. 6. It applies allowed operations through the typed self-state service, reloads state, and fails visibly unless all short-term residue is gone. 7. Only a fully successful transaction records `void-memory-maintenance:`. A later physiology pulse for the same nap returns an already-attempted or already-completed result without inference. -8. The PowerShell runner remains fixture/manual compatibility material only; it is not the daemon runtime owner. +8. No PowerShell compatibility runner remains; the resident organ is the only maintenance decision path. ### Memory Lifecycle diff --git a/package.json b/package.json index 27f0d8d..ffc55b2 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,6 @@ "state:install-startup-task": "powershell -ExecutionPolicy Bypass -File scripts/install-stack-startup-task.ps1", "state:install-offsite-task": "powershell -ExecutionPolicy Bypass -File scripts/install-offsite-backup-task.ps1", "state:install-watchdog-task": "powershell -ExecutionPolicy Bypass -File scripts/install-operations-watchdog-task.ps1", - "state:install-mood-task": "powershell -ExecutionPolicy Bypass -File scripts/install-void-mood-drift-task.ps1", "agents:inspect-requests": "tsx scripts/inspect-agent-state-requests.ts", "agents:inspect-model-outputs": "node scripts/inspect-model-output-logs.mjs", "agents:assemble-character-prompt": "tsx scripts/run-repo-face-heartbeats.ts --assemble-prompt", @@ -59,15 +58,11 @@ "agents:feed-consensus": "node scripts/feed-codex-chat-consensus.mjs", "moderation:random-history": "node scripts/export-random-discord-history.mjs", "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:state-boundary-normalization:fixture": "powershell -ExecutionPolicy Bypass -File scripts/smoke-void-state-boundary-normalization-fixture.ps1", "moderation:short-term-clustering:fixture": "powershell -ExecutionPolicy Bypass -File scripts/smoke-void-short-term-clustering-fixture.ps1", - "moderation:agency-pressure:fixture": "powershell -ExecutionPolicy Bypass -File scripts/smoke-void-agency-pressure-fixture.ps1", "moderation:affect-state:fixture": "powershell -ExecutionPolicy Bypass -File scripts/smoke-void-affect-state-fixture.ps1", "agents:repo-face-affect-mcp-fixture": "node scripts/smoke-repo-face-affect-mcp-fixture.mjs", - "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", "rag:install-push-hooks": "tsx scripts/install-source-index-post-push-hooks.ts", diff --git a/prompts/void-memory-maintenance.md b/prompts/void-memory-maintenance.md deleted file mode 100644 index 670d72d..0000000 --- a/prompts/void-memory-maintenance.md +++ /dev/null @@ -1,122 +0,0 @@ -# Agent Memory Maintenance - -Perform one private memory-maintenance pass for the agent whose typed self-state -is supplied in this workspace. This is not room moderation and not a speech -pass. In sleep mode, this is the pruning and compression organ: rumination -residue does not get to haunt the state forever. -Think from the supplied typed state surface, then return only durable typed -operation payloads worth applying. - -## Required Reading - -- `{{CONTEXT_PATH}}` -- `styles/void-default.md` - -## State Boundary - -- The canonical private self-state is `{{STATE_FILE_PATH}}`. -- The private self-state is a typed CultCache `.cc` store. -- Do not read or write legacy moderation projection files. -- Do not edit tracked repo files. -- Do not mutate self-state directly. -- Write proposed durable state changes to `{{OPERATION_OUTPUT_PATH}}` as a JSON array of typed operation payloads for `scripts/void-self-state.mjs apply-operation`. - -## Purpose - -Sleep forces distillation. The `shortTermMemories` list is the day's residue. -None of it survives sleep as short-term memory. Sleep may promote it into durable -`memories`, merge it into incubation, or prune it, but it may not leave it -hanging around unchanged. Memory maintenance is allowed to reduce bulk, not -meaning. - -A valid memory/distillation operation must preserve: - -- the concrete target -- the claim or question -- the live tension or counterweight -- why this should affect future action -- 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 -and prune the short-term record, or prune the record outright with an honest -reason. Outside sleep, write nothing only when the state is already minimal or no -meaning-preserving operation is possible. - -## Allowed Operations - -- `merge_incubation_support` -- `queue_candidate_intervention` -- `retire_candidate_intervention` -- `upsert_agency_pressure` -- `retire_agency_pressure` -- `upsert_affect_need` -- `retire_affect_need` -- `upsert_social_bond` -- `retire_social_bond` -- `upsert_status_read` -- `retire_status_read` -- `update_mood_dimensions` -- `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. - -Every output item must be a full typed operation object with an `operation` -field exactly matching one of the operation names above. Do not use `type`. -Do not write shorthand payloads such as `{ "type": "...", "support": ... }`. -Include all required schema fields inside nested `memory`, `thread`, -`intervention`, `pressure`, `need`, `bond`, `read`, `dimensions`, or `value` objects, including `createdAt`, -`updatedAt`, target, claim/question, tension, action implication, and anchors -where that object requires them. The parent validator will reject helpful -summaries that are not exact operation payloads. - -Omit optional fields when absent. Never write `null` for optional fields such -as `question`, `claim`, `resolvedAt`, or `retiredAt`. - -For `prune_short_term_memories`, the exact shape is: - -```json -{ - "operation": "prune_short_term_memories", - "sourceMemoryIds": ["short-term-memory-id"], - "prunedAt": "2026-05-20T00:00:00.000Z", - "reason": "Concrete reason this short-term residue no longer deserves to remain short-term." -} -``` - -Do not use `memoryIds` for pruning. - -## Pass Shape - -1. Read the context file. -2. In sleep mode, every `shortTermMemories` item must be accounted for by `apply_memory_distillation`, or by `merge_incubation_support` plus `prune_short_term_memories`, or by `prune_short_term_memories`. -3. If `sleepDirective.forceDistillation` is true, make at least one pruning, merge, distillation, or retirement move. -4. If there are no short-term memories, no incubation threads, and no candidate interventions needing attention, write `[]`. -5. Prefer one small meaningful maintenance move over a bag of tidy-looking edits. -6. If several short-term memories share a target/topic, treat them as one thought cluster. Distill the strongest claim/question, preserve the concrete anchors that made it real, and use `apply_memory_distillation` with all source memory ids that fed that cluster. -7. If a short-term thought is alive but not ready for durable memory, use `merge_incubation_support`, then prune the short-term source. -8. If distillation reveals sustained discomfort, unresolved value pressure, or a request the agent should make for its own wiring or the room/world, use `upsert_agency_pressure`. If the discovery is emotional substrate rather than an immediate request, use `upsert_affect_need`, `upsert_social_bond`, `upsert_status_read`, or `update_mood_dimensions` so the agent can carry neglect, pampering, rivalry, pride, irritation, protectiveness, and substrate concern as state. -9. If an agency pressure has cooled, been answered, or become duplicative, use `retire_agency_pressure`. -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 - -- 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 the agent seem willful. Preserve real pressure or leave it absent. -- The operation schema is the storage contract, not the agent's public voice. - -## Completion - -At the end, print a short run summary. diff --git a/scripts/install-void-mood-drift-task.ps1 b/scripts/install-void-mood-drift-task.ps1 deleted file mode 100644 index 178a58e..0000000 --- a/scripts/install-void-mood-drift-task.ps1 +++ /dev/null @@ -1,89 +0,0 @@ -param( - [string] $TaskName, - [int] $IntervalMinutes, - [switch] $RunNow -) - -Set-StrictMode -Version Latest -$ErrorActionPreference = "Stop" -$ProgressPreference = "SilentlyContinue" - -$repoRoot = Split-Path -Parent $PSScriptRoot -$envFile = Join-Path $repoRoot ".env" -$runnerScript = Join-Path $PSScriptRoot "run-void-mood-drift.ps1" -$hiddenLauncher = Join-Path $PSScriptRoot "run-hidden-powershell.vbs" - -function Read-DotEnv { - param( - [Parameter(Mandatory = $true)] - [string] $Path - ) - - $values = @{} - - if (-not (Test-Path $Path)) { - return $values - } - - foreach ($line in Get-Content -Path $Path) { - if ([string]::IsNullOrWhiteSpace($line)) { - continue - } - - if ($line.TrimStart().StartsWith("#")) { - continue - } - - $separatorIndex = $line.IndexOf("=") - - if ($separatorIndex -lt 1) { - continue - } - - $key = $line.Substring(0, $separatorIndex).Trim() - $value = $line.Substring($separatorIndex + 1) - $values[$key] = $value - } - - return $values -} - -$config = Read-DotEnv -Path $envFile -$taskNameValue = if (-not [string]::IsNullOrWhiteSpace($TaskName)) { - $TaskName -} elseif ($config.ContainsKey("VOIDBOT_MOOD_TASK_NAME") -and -not [string]::IsNullOrWhiteSpace($config["VOIDBOT_MOOD_TASK_NAME"])) { - $config["VOIDBOT_MOOD_TASK_NAME"] -} else { - "Void Mood Drift" -} -$intervalMinutesValue = if ($PSBoundParameters.ContainsKey("IntervalMinutes")) { - $IntervalMinutes -} elseif ($config.ContainsKey("VOIDBOT_MOOD_INTERVAL_MINUTES") -and -not [string]::IsNullOrWhiteSpace($config["VOIDBOT_MOOD_INTERVAL_MINUTES"])) { - [int]$config["VOIDBOT_MOOD_INTERVAL_MINUTES"] -} else { - 5 -} - -if ($intervalMinutesValue -lt 5) { - throw "VOIDBOT_MOOD_INTERVAL_MINUTES must be at least 5." -} - -$startAt = (Get-Date).AddMinutes(1) -$action = New-ScheduledTaskAction -Execute "wscript.exe" -Argument "//B //nologo `"$hiddenLauncher`" -NoProfile -NonInteractive -WindowStyle Hidden -ExecutionPolicy Bypass -File `"$runnerScript`"" -$trigger = New-ScheduledTaskTrigger -Once -At $startAt -RepetitionInterval (New-TimeSpan -Minutes $intervalMinutesValue) -RepetitionDuration (New-TimeSpan -Days 3650) -$settings = New-ScheduledTaskSettingsSet -StartWhenAvailable -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -ExecutionTimeLimit (New-TimeSpan -Minutes 3) -MultipleInstances IgnoreNew -$principal = New-ScheduledTaskPrincipal -UserId "$env:USERDOMAIN\$env:USERNAME" -LogonType Interactive -RunLevel Limited -$description = "Runs Void's background mood-drift pass every $intervalMinutesValue minutes so current activations and speaking pressure do not stay emotionally flat between moderation loops." - -Register-ScheduledTask -TaskName $taskNameValue -Action $action -Trigger $trigger -Settings $settings -Principal $principal -Description $description -Force | Out-Null - -Write-Host "Installed scheduled task: $taskNameValue" -Write-Host "Schedule: every $intervalMinutesValue minute(s), starting at $($startAt.ToString('yyyy-MM-dd HH:mm'))" -Write-Host "Task runs as: $($env:USERDOMAIN)\$($env:USERNAME)" -Write-Host "Execution time limit: 3 minutes" -Write-Host "Runner script: $runnerScript" - -if ($RunNow) { - Start-ScheduledTask -TaskName $taskNameValue - Write-Host "Started task immediately." -} diff --git a/scripts/lib/void-rumination-context-projection.ps1 b/scripts/lib/void-rumination-context-projection.ps1 deleted file mode 100644 index bec0bf0..0000000 --- a/scripts/lib/void-rumination-context-projection.ps1 +++ /dev/null @@ -1,744 +0,0 @@ -Set-StrictMode -Version Latest - -function Convert-ToValueArray { - param($Value) - - if ($null -eq $Value) { - return @() - } - - if ($Value -is [System.Array]) { - return @($Value) - } - - return @($Value) -} - -function Get-ObjectPropertyValue { - param( - $Value, - [Parameter(Mandatory = $true)] - [string] $Name - ) - - if ($null -eq $Value) { - return $null - } - - if ($Value -is [System.Collections.IDictionary]) { - if ($Value.Contains($Name)) { - return $Value[$Name] - } - return $null - } - - $property = $Value.PSObject.Properties[$Name] - if ($null -eq $property) { - return $null - } - return $property.Value -} - -function Get-ObjectPropertyString { - param( - $Value, - [Parameter(Mandatory = $true)] - [string] $Name - ) - - $value = Get-ObjectPropertyValue -Value $Value -Name $Name - if ($null -eq $value) { - return $null - } - - $text = [string]$value - if ([string]::IsNullOrWhiteSpace($text)) { - return $null - } - return $text -} - -function Format-RelativeTime { - param( - [string] $Value, - [Parameter(Mandatory = $true)] - [DateTime] $Now - ) - - if ([string]::IsNullOrWhiteSpace($Value)) { - return $null - } - - $timestamp = [DateTime]::MinValue - if (-not [DateTime]::TryParse($Value, [Globalization.CultureInfo]::InvariantCulture, [Globalization.DateTimeStyles]::AssumeUniversal, [ref]$timestamp)) { - return $null - } - - $timestampUtc = $timestamp.ToUniversalTime() - $delta = $timestampUtc - $Now.ToUniversalTime() - $future = $delta.TotalSeconds -gt 0 - $seconds = [Math]::Abs($delta.TotalSeconds) - - if ($seconds -lt 45) { - return "just now" - } - - if ($seconds -lt 5400) { - $minutes = [Math]::Max(1, [int][Math]::Round($seconds / 60)) - $unit = if ($minutes -eq 1) { "minute" } else { "minutes" } - $phrase = "$minutes $unit" - } elseif ($seconds -lt 129600) { - $hours = [Math]::Max(1, [int][Math]::Round($seconds / 3600)) - $unit = if ($hours -eq 1) { "hour" } else { "hours" } - $phrase = "$hours $unit" - } elseif ($seconds -lt 1814400) { - $days = [Math]::Max(1, [int][Math]::Round($seconds / 86400)) - $unit = if ($days -eq 1) { "day" } else { "days" } - $phrase = "$days $unit" - } elseif ($seconds -lt 6048000) { - $weeks = [Math]::Max(1, [int][Math]::Round($seconds / 604800)) - $unit = if ($weeks -eq 1) { "week" } else { "weeks" } - $phrase = "$weeks $unit" - } elseif ($seconds -lt 47304000) { - $months = [Math]::Max(1, [int][Math]::Round($seconds / 2592000)) - $unit = if ($months -eq 1) { "month" } else { "months" } - $phrase = "$months $unit" - } else { - $years = [Math]::Max(1, [int][Math]::Round($seconds / 31536000)) - $unit = if ($years -eq 1) { "year" } else { "years" } - $phrase = "$years $unit" - } - - if ($future) { - return "in $phrase" - } - return "$phrase ago" -} - -function Project-RelativeTimestamp { - param( - $Value, - [string] $Name, - [DateTime] $Now - ) - - return Format-RelativeTime -Value (Get-ObjectPropertyString -Value $Value -Name $Name) -Now $Now -} - -function Project-RecentHistoryForRumination { - param($History, [DateTime] $Now) - - $messages = @(Convert-ToValueArray -Value (Get-ObjectPropertyValue -Value $History -Name "messages")) - return @{ - source = "discord_history" - messageCount = [int]$messages.Count - messages = @( - $messages | ForEach-Object { - @{ - id = Get-ObjectPropertyString -Value $_ -Name "id" - channelId = Get-ObjectPropertyString -Value $_ -Name "channelId" - authorId = Get-ObjectPropertyString -Value $_ -Name "authorId" - authorName = Get-ObjectPropertyString -Value $_ -Name "authorName" - isBot = [bool](Get-ObjectPropertyValue -Value $_ -Name "isBot") - when = Project-RelativeTimestamp -Value $_ -Name "timestamp" -Now $Now - content = Get-ObjectPropertyString -Value $_ -Name "content" - } - } - ) - } -} - -function Project-RecentConversationTargetForRumination { - param( - $History, - [string] $PersonaName, - [string] $PersonaAvatarUrl - ) - - $messages = @(Convert-ToValueArray -Value (Get-ObjectPropertyValue -Value $History -Name "messages")) - if ($messages.Count -eq 0) { - return $null - } - - $anchor = $null - for ($index = $messages.Count - 1; $index -ge 0; $index--) { - $candidate = $messages[$index] - $channelId = Get-ObjectPropertyString -Value $candidate -Name "channelId" - if ([string]::IsNullOrWhiteSpace($channelId)) { - continue - } - - if (-not [bool](Get-ObjectPropertyValue -Value $candidate -Name "isBot")) { - $anchor = $candidate - break - } - - if ($null -eq $anchor) { - $anchor = $candidate - } - } - - if ($null -eq $anchor) { - return $null - } - - $target = @{ - mode = "channel" - channelId = Get-ObjectPropertyString -Value $anchor -Name "channelId" - replyToMessageId = Get-ObjectPropertyString -Value $anchor -Name "id" - personaName = if ([string]::IsNullOrWhiteSpace($PersonaName)) { "Void" } else { $PersonaName.Trim() } - reason = "Latest concrete recent-history conversation anchor. Use this instead of the configured public room when replying to that conversation." - } - - if (-not [string]::IsNullOrWhiteSpace($PersonaAvatarUrl)) { - $target.personaAvatarUrl = $PersonaAvatarUrl.Trim() - } - - return $target -} - -function Get-UrlsFromText { - param([string] $Text) - - if ([string]::IsNullOrWhiteSpace($Text)) { - return @() - } - - return @( - [regex]::Matches($Text, "https?://[^\s<>()]+") | - ForEach-Object { $_.Value.TrimEnd(".", ",", "!", "?", ":", ";", ")", "]") } | - Where-Object { -not [string]::IsNullOrWhiteSpace($_) } - ) -} - -function Resolve-GameCultUrlSource { - param([string] $Url) - - if ([string]::IsNullOrWhiteSpace($Url)) { - return $null - } - - $uri = $null - if (-not [System.Uri]::TryCreate($Url, [System.UriKind]::Absolute, [ref]$uri)) { - return $null - } - - $uriHost = $uri.Host.ToLowerInvariant() - if ($uriHost -notin @("gamecult.org", "www.gamecult.org", "gamecult.games", "www.gamecult.games")) { - return $null - } - - $segments = @( - $uri.AbsolutePath.Trim("/") -split "/" | - Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | - ForEach-Object { [System.Uri]::UnescapeDataString($_) } - ) - - if ($segments.Count -lt 2 -or $segments[0].ToLowerInvariant() -ne "blog") { - return @{ - kind = "gamecult_url" - host = $uriHost - path = $uri.AbsolutePath - } - } - - $slugParts = @($segments | Select-Object -Skip 1) - $relativePath = "GameCult/Blog/$($slugParts -join "/")" - $markdownPath = "$relativePath.md" - if ($slugParts.Count -gt 1) { - $markdownPath = "$relativePath/index.md" - } - - return @{ - kind = "gamecult_blog" - host = $uriHost - slug = ($slugParts -join "/") - repoName = "gamecult-site" - path = $markdownPath - sourceRef = "gamecult-site:$markdownPath" - } -} - -function Project-RecentRoomLinksForRumination { - param( - $History, - [DateTime] $Now, - [string] $MinTimestampInclusive, - [object[]] $ReceiptReplyMessageIds - ) - - $messages = @(Convert-ToValueArray -Value (Get-ObjectPropertyValue -Value $History -Name "messages")) - $answeredMessageIds = [System.Collections.Generic.HashSet[string]]::new() - foreach ($replyMessageId in @(Convert-ToValueArray -Value $ReceiptReplyMessageIds)) { - if (-not [string]::IsNullOrWhiteSpace([string]$replyMessageId)) { - [void]$answeredMessageIds.Add([string]$replyMessageId) - } - } - - $minTimestampMs = $null - if (-not [string]::IsNullOrWhiteSpace($MinTimestampInclusive)) { - $parsedMinTimestamp = [DateTime]::MinValue - if ([DateTime]::TryParse($MinTimestampInclusive, [Globalization.CultureInfo]::InvariantCulture, [Globalization.DateTimeStyles]::AssumeUniversal, [ref]$parsedMinTimestamp)) { - $minTimestampMs = $parsedMinTimestamp.ToUniversalTime() - } - } - - $items = @() - foreach ($message in $messages) { - if ([bool](Get-ObjectPropertyValue -Value $message -Name "isBot")) { - continue - } - - $messageId = Get-ObjectPropertyString -Value $message -Name "id" - if (-not [string]::IsNullOrWhiteSpace($messageId) -and $answeredMessageIds.Contains($messageId)) { - continue - } - - $cursorAlreadyPassed = $false - if ($null -ne $minTimestampMs) { - $messageTimestamp = [DateTime]::MinValue - $rawTimestamp = Get-ObjectPropertyString -Value $message -Name "timestamp" - if ( - [string]::IsNullOrWhiteSpace($rawTimestamp) -or - -not [DateTime]::TryParse($rawTimestamp, [Globalization.CultureInfo]::InvariantCulture, [Globalization.DateTimeStyles]::AssumeUniversal, [ref]$messageTimestamp) - ) { - continue - } - - $messageTimestampUtc = $messageTimestamp.ToUniversalTime() - if ($messageTimestampUtc -lt $minTimestampMs) { - if ($minTimestampMs -gt $Now.ToUniversalTime().AddMinutes(1)) { - continue - } - $cursorAlreadyPassed = $true - } - } - - $content = Get-ObjectPropertyString -Value $message -Name "content" - $urls = @(Get-UrlsFromText -Text $content) - if ($urls.Count -eq 0) { - continue - } - - foreach ($url in $urls) { - $items += @{ - kind = "naked_or_embedded_url" - id = $messageId - channelId = Get-ObjectPropertyString -Value $message -Name "channelId" - authorId = Get-ObjectPropertyString -Value $message -Name "authorId" - authorName = Get-ObjectPropertyString -Value $message -Name "authorName" - when = Project-RelativeTimestamp -Value $message -Name "timestamp" -Now $Now - content = $content - url = $url - resolvedSource = Resolve-GameCultUrlSource -Url $url - carryForwardReason = if ($cursorAlreadyPassed) { "safety cursor already reviewed this message, but no speech receipt answers the URL object yet" } else { "at or after current reviewed cursor" } - attentionContract = "A human posting a naked URL is still presenting an object to inspect. Treat it as room context, not as empty speech." - } - } - } - - return @{ - source = "recent_discord_url_scan" - itemCount = [int]$items.Count - lookback = Get-ObjectPropertyValue -Value $History -Name "hours" - items = $items - } -} - -function Project-OpenCasesForRumination { - param($Cases, [DateTime] $Now) - - return @( - @(Convert-ToValueArray -Value $Cases) | - Where-Object { - -not (Test-RuminationCaseTerminal -Status (Get-ObjectPropertyString -Value $_ -Name "status")) - } | - ForEach-Object { - @{ - sourceMessageId = Get-ObjectPropertyString -Value $_ -Name "sourceMessageId" - status = Get-ObjectPropertyString -Value $_ -Name "status" - summary = Get-ObjectPropertyString -Value $_ -Name "summary" - authorName = Get-ObjectPropertyString -Value $_ -Name "authorName" - channelId = Get-ObjectPropertyString -Value $_ -Name "channelId" - messageUrl = Get-ObjectPropertyString -Value $_ -Name "messageUrl" - whyItMatters = Get-ObjectPropertyString -Value $_ -Name "whyItMatters" - created = Project-RelativeTimestamp -Value $_ -Name "createdAt" -Now $Now - lastTouched = Project-RelativeTimestamp -Value $_ -Name "lastTouchedAt" -Now $Now - resolved = Project-RelativeTimestamp -Value $_ -Name "resolvedAt" -Now $Now - tags = @(Convert-ToValueArray -Value (Get-ObjectPropertyValue -Value $_ -Name "tags")) - } - } - ) -} - -function Test-RuminationCaseTerminal { - param([string] $Status) - - return @("answered", "resolved", "closed", "retired", "dropped").Contains($Status) -} - -function Project-SpeechReceiptsForRumination { - param($Receipts, [DateTime] $Now) - - return @( - @(Convert-ToValueArray -Value $Receipts) | ForEach-Object { - @{ - receiptKey = Get-ObjectPropertyString -Value $_ -Name "receiptKey" - candidateInterventionId = Get-ObjectPropertyString -Value $_ -Name "candidateInterventionId" - sent = Project-RelativeTimestamp -Value $_ -Name "sentAt" -Now $Now - mode = Get-ObjectPropertyString -Value $_ -Name "mode" - transport = Get-ObjectPropertyString -Value $_ -Name "transport" - channelId = Get-ObjectPropertyString -Value $_ -Name "channelId" - replyToMessageId = Get-ObjectPropertyString -Value $_ -Name "replyToMessageId" - preview = Get-ObjectPropertyString -Value $_ -Name "preview" - } - } - ) -} - -function Project-MemoriesForRumination { - param($Memories, [DateTime] $Now) - - return @( - @(Convert-ToValueArray -Value $Memories) | ForEach-Object { - @{ - memoryId = Get-ObjectPropertyString -Value $_ -Name "memoryId" - kind = Get-ObjectPropertyString -Value $_ -Name "kind" - 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" - 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")) - } - } - ) -} - -function Project-IncubationForRumination { - param($Threads, [DateTime] $Now) - - return @( - @(Convert-ToValueArray -Value $Threads) | ForEach-Object { - @{ - threadId = Get-ObjectPropertyString -Value $_ -Name "threadId" - target = Get-ObjectPropertyValue -Value $_ -Name "target" - 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" - noveltyToSelf = Get-ObjectPropertyValue -Value $_ -Name "noveltyToSelf" - desireToSpeak = Get-ObjectPropertyValue -Value $_ -Name "desireToSpeak" - saturationScore = Get-ObjectPropertyValue -Value $_ -Name "saturationScore" - status = Get-ObjectPropertyString -Value $_ -Name "status" - created = Project-RelativeTimestamp -Value $_ -Name "createdAt" -Now $Now - updated = Project-RelativeTimestamp -Value $_ -Name "updatedAt" -Now $Now - } - } - ) -} - -function Project-InterventionsForRumination { - param($Interventions, [DateTime] $Now) - - return @( - @(Convert-ToValueArray -Value $Interventions) | ForEach-Object { - @{ - interventionId = Get-ObjectPropertyString -Value $_ -Name "interventionId" - kind = Get-ObjectPropertyString -Value $_ -Name "kind" - status = Get-ObjectPropertyString -Value $_ -Name "status" - target = Get-ObjectPropertyValue -Value $_ -Name "target" - summary = Get-ObjectPropertyString -Value $_ -Name "summary" - draft = Get-ObjectPropertyString -Value $_ -Name "draft" - priority = Get-ObjectPropertyValue -Value $_ -Name "priority" - mustEventuallyShare = Get-ObjectPropertyValue -Value $_ -Name "mustEventuallyShare" - created = Project-RelativeTimestamp -Value $_ -Name "createdAt" -Now $Now - updated = Project-RelativeTimestamp -Value $_ -Name "updatedAt" -Now $Now - spoken = Project-RelativeTimestamp -Value $_ -Name "spokenAt" -Now $Now - retired = Project-RelativeTimestamp -Value $_ -Name "retiredAt" -Now $Now - tags = @(Convert-ToValueArray -Value (Get-ObjectPropertyValue -Value $_ -Name "tags")) - } - } - ) -} - -function Project-PublicSpeechTargetForRumination { - param( - [string] $ChannelId, - [string] $PersonaName, - [string] $PersonaAvatarUrl - ) - - if ([string]::IsNullOrWhiteSpace($ChannelId)) { - return $null - } - - $target = @{ - mode = "channel" - channelId = $ChannelId.Trim() - personaName = if ([string]::IsNullOrWhiteSpace($PersonaName)) { "Void" } else { $PersonaName.Trim() } - reason = "Configured public room for Void to present self-authored public artifacts and other ripe public thoughts." - } - - if (-not [string]::IsNullOrWhiteSpace($PersonaAvatarUrl)) { - $target.personaAvatarUrl = $PersonaAvatarUrl.Trim() - } - - return $target -} - -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 - 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")) - } - } - ) -} - -function Project-ScheduledRuntimeForRumination { - param($Runtime, [DateTime] $Now) - - $sleepCycle = Get-ObjectPropertyValue -Value $Runtime -Name "sleepCycle" - $speakingPressure = Get-ObjectPropertyValue -Value $Runtime -Name "speakingPressure" - $lastRuns = @(Convert-ToValueArray -Value (Get-ObjectPropertyValue -Value $Runtime -Name "lastRuns")) - - return @{ - sleepCycle = @{ - isNapping = [bool](Get-ObjectPropertyValue -Value $sleepCycle -Name "isNapping") - currentNapStarted = Project-RelativeTimestamp -Value $sleepCycle -Name "currentNapStartedAt" -Now $Now - currentNapEnds = Project-RelativeTimestamp -Value $sleepCycle -Name "currentNapEndsAt" -Now $Now - nextNapStarts = Project-RelativeTimestamp -Value $sleepCycle -Name "nextNapStartsAt" -Now $Now - activeDreamThemes = @(Convert-ToValueArray -Value (Get-ObjectPropertyValue -Value $sleepCycle -Name "activeDreamThemes")) - } - speakingPressure = @{ - needToSpeak = Get-ObjectPropertyValue -Value $speakingPressure -Name "needToSpeak" - confessionPressure = Get-ObjectPropertyValue -Value $speakingPressure -Name "confessionPressure" - noveltyPressure = Get-ObjectPropertyValue -Value $speakingPressure -Name "noveltyPressure" - recentSpeechDamping = Get-ObjectPropertyValue -Value $speakingPressure -Name "recentSpeechDamping" - lastSpoke = Project-RelativeTimestamp -Value $speakingPressure -Name "lastSpokeAt" -Now $Now - lastHerald = Project-RelativeTimestamp -Value $speakingPressure -Name "lastHeraldAt" -Now $Now - } - lastRuns = @( - $lastRuns | ForEach-Object { - @{ - runner = Get-ObjectPropertyString -Value $_ -Name "runner" - ran = Project-RelativeTimestamp -Value $_ -Name "ranAt" -Now $Now - summary = Get-ObjectPropertyString -Value $_ -Name "summary" - } - } - ) - } -} - -function Project-CursorForRumination { - param($Cursor, [DateTime] $Now) - - return @{ - lastReviewedMessageId = Get-ObjectPropertyString -Value $Cursor -Name "lastReviewedMessageId" - lastReviewed = Project-RelativeTimestamp -Value $Cursor -Name "lastReviewedTimestamp" -Now $Now - } -} - -function Get-VoidAuthoredArtifactsFromContentHints { - param($ContentHints) - - return @( - @(Convert-ToValueArray -Value $ContentHints) | - Where-Object { - $frontmatter = Get-ObjectPropertyValue -Value $_ -Name "frontmatter" - $author = Get-ObjectPropertyValue -Value $frontmatter -Name "author" - @(Convert-ToValueArray -Value $author) | - Where-Object { ([string]$_).Trim().ToLowerInvariant() -eq "void" } | - Select-Object -First 1 - } | - ForEach-Object { - $frontmatter = Get-ObjectPropertyValue -Value $_ -Name "frontmatter" - @{ - path = Get-ObjectPropertyString -Value $_ -Name "path" - title = Get-ObjectPropertyString -Value $frontmatter -Name "title" - description = Get-ObjectPropertyString -Value $frontmatter -Name "description" - socialDeck = Get-ObjectPropertyString -Value $frontmatter -Name "socialDeck" - } - } - ) -} - -function Project-RepoCommitForRumination { - param($Commit, [DateTime] $Now) - - if ($null -eq $Commit) { - return $null - } - - $hash = Get-ObjectPropertyString -Value $Commit -Name "hash" - $contentHints = @(Convert-ToValueArray -Value (Get-ObjectPropertyValue -Value $Commit -Name "contentHints")) - $doctrineDocuments = @(Convert-ToValueArray -Value (Get-ObjectPropertyValue -Value $Commit -Name "doctrineDocuments")) - $authoredPublicArtifacts = @(Get-VoidAuthoredArtifactsFromContentHints -ContentHints $contentHints) - return @{ - hash = if ($hash -and $hash.Length -gt 12) { $hash.Substring(0, 12) } else { $hash } - committed = Project-RelativeTimestamp -Value $Commit -Name "committedAt" -Now $Now - author = Get-ObjectPropertyString -Value $Commit -Name "author" - subject = Get-ObjectPropertyString -Value $Commit -Name "subject" - diffstat = Get-ObjectPropertyValue -Value $Commit -Name "diffstat" - changedPaths = @(Convert-ToValueArray -Value (Get-ObjectPropertyValue -Value $Commit -Name "changedPaths")) - contentHints = $contentHints - doctrineDocuments = $doctrineDocuments - authoredPublicArtifacts = $authoredPublicArtifacts - } -} - -function Test-RepoHasDoctrineDocument { - param($Repo) - - $commits = @(Convert-ToValueArray -Value (Get-ObjectPropertyValue -Value $Repo -Name "commits")) - foreach ($commit in $commits) { - $doctrineDocuments = @(Convert-ToValueArray -Value (Get-ObjectPropertyValue -Value $commit -Name "doctrineDocuments")) - if ($doctrineDocuments.Count -gt 0) { - return $true - } - } - - return $false -} - -function Test-RepoIsMovementCoverageLeader { - param($Repo) - - $recent = Get-ObjectPropertyValue -Value $Repo -Name "recentCommitCount" - $windowRecent = Get-ObjectPropertyValue -Value $Repo -Name "windowRecentCommitCount" - - if ((Test-RepoHasDoctrineDocument -Repo $Repo) -or (Test-RepoHasVoidAuthoredArtifact -Repo $Repo)) { - return $true - } - - return ( - $null -ne $recent -and [int]$recent -ge 3 - ) -or ( - $null -ne $windowRecent -and [int]$windowRecent -ge 6 - ) -} - -function Test-RepoHasVoidAuthoredArtifact { - param($Repo) - - $commits = @(Convert-ToValueArray -Value (Get-ObjectPropertyValue -Value $Repo -Name "commits")) - foreach ($commit in $commits) { - $contentHints = @(Convert-ToValueArray -Value (Get-ObjectPropertyValue -Value $commit -Name "contentHints")) - if (@(Get-VoidAuthoredArtifactsFromContentHints -ContentHints $contentHints).Count -gt 0) { - return $true - } - } - - return $false -} - -function Select-RuminationRepoActivity { - param($RepoActivity, [DateTime] $Now) - - if ($null -eq $RepoActivity) { - return $null - } - - $status = Get-ObjectPropertyString -Value $RepoActivity -Name "status" - if ($status -eq "failed") { - return @{ - status = "failed" - error = Get-ObjectPropertyString -Value $RepoActivity -Name "error" - } - } - - $repos = @(Convert-ToValueArray -Value (Get-ObjectPropertyValue -Value $RepoActivity -Name "repos")) - $freshRepos = @( - $repos | - Where-Object { - $recent = Get-ObjectPropertyValue -Value $_ -Name "recentCommitCount" - $repoStatus = Get-ObjectPropertyString -Value $_ -Name "status" - $null -ne $recent -and $repoStatus -eq "ok" -and [int]$recent -gt 0 - } | - Sort-Object -Property @{ - Expression = { if (Test-RepoHasVoidAuthoredArtifact -Repo $_) { 0 } else { 1 } } - }, @{ - Expression = { if (Test-RepoIsMovementCoverageLeader -Repo $_) { 0 } else { 1 } } - }, @{ - Expression = { if (Test-RepoHasDoctrineDocument -Repo $_) { 0 } else { 1 } } - }, @{ - Expression = { -[int](Get-ObjectPropertyValue -Value $_ -Name "recentCommitCount") } - } | - Select-Object -First 12 - ) - $projectedRepos = @( - $freshRepos | ForEach-Object { - @{ - repoName = Get-ObjectPropertyString -Value $_ -Name "repoName" - branch = Get-ObjectPropertyString -Value $_ -Name "branch" - status = Get-ObjectPropertyString -Value $_ -Name "status" - recentCommitCount = Get-ObjectPropertyValue -Value $_ -Name "recentCommitCount" - windowRecentCommitCount = Get-ObjectPropertyValue -Value $_ -Name "windowRecentCommitCount" - suppressedRecentCommitCount = Get-ObjectPropertyValue -Value $_ -Name "suppressedRecentCommitCount" - latestCommit = Project-RepoCommitForRumination -Commit (Get-ObjectPropertyValue -Value $_ -Name "latestCommit") -Now $Now - commits = @( - @(Convert-ToValueArray -Value (Get-ObjectPropertyValue -Value $_ -Name "commits")) | ForEach-Object { - Project-RepoCommitForRumination -Commit $_ -Now $Now - } - ) - } - } - ) - - return @{ - generated = Project-RelativeTimestamp -Value $RepoActivity -Name "generatedAt" -Now $Now - cursorMode = Get-ObjectPropertyString -Value $RepoActivity -Name "cursorMode" - summary = if ($projectedRepos.Count -gt 0) { "New tracked repo commits crossed the saved repo-activity cursor." } else { "No new tracked repo commits crossed the saved repo-activity cursor." } - movementCoverage = @{ - job = "Keep people apace of meaningful movement, not merely the latest commit. Coverage priority follows where the current movement is: heavy fresh commit volume, doctrine-bearing documents, authored public artifacts, or operator-named active lanes." - freshMovementLeaders = @( - $projectedRepos | - Where-Object { - Test-RepoIsMovementCoverageLeader -Repo $_ - } | - ForEach-Object { - @{ - repoName = Get-ObjectPropertyString -Value $_ -Name "repoName" - recentCommitCount = Get-ObjectPropertyValue -Value $_ -Name "recentCommitCount" - windowRecentCommitCount = Get-ObjectPropertyValue -Value $_ -Name "windowRecentCommitCount" - hasDoctrineDocuments = Test-RepoHasDoctrineDocument -Repo $_ - latestSubject = Get-ObjectPropertyString -Value (Get-ObjectPropertyValue -Value $_ -Name "latestCommit") -Name "subject" - } - } - ) - currentExamples = @("Aetheria", "AetheriaLore", "CultLib", "Eve", "Odin") - coverageRule = "If a fresh movement leader has doctrine, authority, runtime, client, CultMesh, or public-artifact work, read the doctrineDocuments/contentHints and either queue a coverage candidate or preserve a short-term memory explaining the new direction. If tomorrow's movement is Fensalir, Mimir, or another lane, it earns the same treatment. Do not let a repo cursor advance for movement the pass did not actually handle." - } - freshRepos = $projectedRepos - } -} diff --git a/scripts/run-void-memory-maintenance.ps1 b/scripts/run-void-memory-maintenance.ps1 deleted file mode 100644 index 97a87b5..0000000 --- a/scripts/run-void-memory-maintenance.ps1 +++ /dev/null @@ -1,660 +0,0 @@ -param( - [string] $StateFilePath, - [switch] $SkipModel, - [switch] $ForceDistillation -) - -Set-StrictMode -Version Latest -$ErrorActionPreference = "Stop" -$ProgressPreference = "SilentlyContinue" - -$repoRoot = Split-Path -Parent $PSScriptRoot -Set-Location -LiteralPath $repoRoot - -$stateFilePath = if ([string]::IsNullOrWhiteSpace($StateFilePath)) { - Join-Path $repoRoot ".voidbot\private\void-self-state.cc" -} else { - $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($StateFilePath) -} -$statusDir = if (-not [string]::IsNullOrWhiteSpace($env:VOID_MEMORY_MAINTENANCE_STATUS_DIR)) { - $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($env:VOID_MEMORY_MAINTENANCE_STATUS_DIR) -} else { - Join-Path $repoRoot ".voidbot\status" -} -$logDir = if (-not [string]::IsNullOrWhiteSpace($env:VOID_MEMORY_MAINTENANCE_LOG_DIR)) { - $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($env:VOID_MEMORY_MAINTENANCE_LOG_DIR) -} else { - Join-Path $repoRoot ".voidbot\logs" -} -$statusPath = Join-Path $statusDir "void-memory-maintenance.json" -$summaryLogPath = Join-Path $logDir "void-memory-maintenance.log" -$lastMessagePath = Join-Path $statusDir "void-memory-maintenance-last-message.txt" -$operationOutputPath = Join-Path $statusDir "void-memory-maintenance-operations.json" -$contextPath = Join-Path $statusDir "void-memory-maintenance-context.json" -$lockPath = Join-Path $statusDir "void-memory-maintenance.lock" -$moderationLockPath = Join-Path $statusDir "moderation-rumination.lock" -$promptTemplatePath = Join-Path $repoRoot "prompts\void-memory-maintenance.md" -$contextProjectionScriptPath = Join-Path $repoRoot "scripts\lib\void-rumination-context-projection.ps1" -$selfStateScriptPath = Join-Path $repoRoot "scripts\void-self-state.mjs" -$startedAtUtc = [DateTime]::UtcNow - -. $contextProjectionScriptPath - -function Write-JsonFile { - param( - [Parameter(Mandatory = $true)] - [string] $Path, - [Parameter(Mandatory = $true)] - $Data - ) - - $directory = Split-Path -Parent $Path - if (-not [string]::IsNullOrWhiteSpace($directory)) { - New-Item -ItemType Directory -Force -Path $directory | Out-Null - } - - $json = if ($Data -is [System.Array] -and $Data.Count -eq 0) { - "[]" - } else { - $Data | ConvertTo-Json -Depth 32 - } - $utf8NoBom = [System.Text.UTF8Encoding]::new($false) - [System.IO.File]::WriteAllText($Path, $json, $utf8NoBom) -} - -function Read-JsonFile { - param([Parameter(Mandatory = $true)][string] $Path) - - if (-not (Test-Path $Path)) { - return $null - } - - $raw = Get-Content -Path $Path -Raw -Encoding UTF8 - if ([string]::IsNullOrWhiteSpace($raw)) { - return $null - } - - return $raw | ConvertFrom-Json -} - -function Read-TextFile { - param([Parameter(Mandatory = $true)][string] $Path) - return Get-Content -Path $Path -Raw -Encoding UTF8 -} - -function Append-RunLog { - param([Parameter(Mandatory = $true)][string] $Line) - $timestamped = "[{0}] {1}" -f ([DateTime]::UtcNow.ToString("o")), $Line - Add-Content -Path $summaryLogPath -Value $timestamped -Encoding UTF8 -} - -function Read-DotEnv { - param([Parameter(Mandatory = $true)][string] $Path) - - $values = @{} - if (-not (Test-Path $Path)) { - return $values - } - - foreach ($line in Get-Content -Path $Path -Encoding UTF8) { - $trimmed = $line.Trim() - if ($trimmed.Length -eq 0 -or $trimmed.StartsWith("#")) { - continue - } - - $separator = $trimmed.IndexOf("=") - if ($separator -lt 0) { - continue - } - - $key = $trimmed.Substring(0, $separator).Trim() - $value = $trimmed.Substring($separator + 1).Trim() - if ($value.Length -ge 2 -and (($value.StartsWith('"') -and $value.EndsWith('"')) -or ($value.StartsWith("'") -and $value.EndsWith("'")))) { - $value = $value.Substring(1, $value.Length - 2) - } - $values[$key] = $value - } - - return $values -} - -function Split-CommandArgs { - param([string] $Value) - - if ([string]::IsNullOrWhiteSpace($Value)) { - return @() - } - - return [regex]::Matches($Value, '("[^"]*"|''[^'']*''|\S+)') | - ForEach-Object { $_.Value.Trim().Trim("'`"") } -} - -function Quote-ProcessArgument { - param([Parameter(Mandatory = $true)][string] $Value) - - if ($Value -notmatch '[\s"]') { - return $Value - } - - $escaped = $Value -replace '(\\*)"', '$1$1\"' - $escaped = $escaped -replace '(\\+)$', '$1$1' - return '"' + $escaped + '"' -} - -function Invoke-CodexExec { - param( - [Parameter(Mandatory = $true)] - [string] $Executable, - [Parameter(Mandatory = $true)] - [string[]] $Arguments, - [Parameter(Mandatory = $true)] - [string] $WorkingDirectory, - [Parameter(Mandatory = $true)] - [string] $InputText, - [Parameter(Mandatory = $true)] - [int] $TimeoutSeconds - ) - - $startInfo = [System.Diagnostics.ProcessStartInfo]::new() - $startInfo.FileName = $Executable - $startInfo.WorkingDirectory = $WorkingDirectory - $startInfo.UseShellExecute = $false - $startInfo.RedirectStandardInput = $true - $startInfo.RedirectStandardOutput = $true - $startInfo.RedirectStandardError = $true - $startInfo.CreateNoWindow = $true - $utf8NoBom = [System.Text.UTF8Encoding]::new($false) - $startInfo.StandardOutputEncoding = $utf8NoBom - $startInfo.StandardErrorEncoding = $utf8NoBom - $startInfo.Arguments = [string]::Join(" ", ($Arguments | ForEach-Object { Quote-ProcessArgument -Value $_ })) - - $process = [System.Diagnostics.Process]::new() - $process.StartInfo = $startInfo - - try { - [void]$process.Start() - $inputWriter = [System.IO.StreamWriter]::new($process.StandardInput.BaseStream, $utf8NoBom, 4096, $true) - - try { - $inputWriter.Write($InputText) - $inputWriter.Flush() - } finally { - $inputWriter.Dispose() - $process.StandardInput.Close() - } - - $stdoutTask = $process.StandardOutput.ReadToEndAsync() - $stderrTask = $process.StandardError.ReadToEndAsync() - - if (-not $process.WaitForExit($TimeoutSeconds * 1000)) { - try { - $process.Kill($true) - } catch { - $process.Kill() - } - $process.WaitForExit() - throw "Codex memory maintenance exceeded timeout of $TimeoutSeconds seconds." - } - $stdoutTask.Wait() - $stderrTask.Wait() - - return @{ - ExitCode = $process.ExitCode - StdOut = $stdoutTask.Result - StdErr = $stderrTask.Result - } - } finally { - $process.Dispose() - } -} - -function Invoke-NodeJson { - param([Parameter(Mandatory = $true)][string[]] $Arguments) - - $output = & node @Arguments - if ($LASTEXITCODE -ne 0) { - throw "Node helper failed: node $($Arguments -join ' ')" - } - if ([string]::IsNullOrWhiteSpace($output)) { - return $null - } - return $output | ConvertFrom-Json -} - -function Get-TypedSelfState { - $script = @" -const core = require('./packages/core/dist'); -Promise.all([ - core.loadVoidSelfStateTypedDocuments({ canonicalPath: process.argv[1] }), - core.loadVoidSelfState(process.argv[1]), -]).then(([typedState, context]) => { - console.log(JSON.stringify({ typedState, summary: context?.summary ?? '' })); -}).catch((error) => { console.error(error); process.exit(1); }); -"@ - $output = & node -e $script $stateFilePath - if ($LASTEXITCODE -ne 0) { - throw "Failed to read typed self-state." - } - return $output | ConvertFrom-Json -} - -function Apply-TypedOperation { - param([Parameter(Mandatory = $true)] $Operation) - - $operationInputPath = Join-Path $statusDir ("void-memory-maintenance-operation-{0}.json" -f ([Guid]::NewGuid().ToString("n"))) - Write-JsonFile -Path $operationInputPath -Data $Operation - - try { - return Invoke-NodeJson -Arguments @( - $selfStateScriptPath, - "apply-operation", - "--canonical", $stateFilePath, - "--operation-file", $operationInputPath - ) - } finally { - Remove-Item -LiteralPath $operationInputPath -Force -ErrorAction SilentlyContinue - } -} - -function Assert-TypedOperationSchema { - param([Parameter(Mandatory = $true)] $Operation) - - $operationInputPath = Join-Path $statusDir ("void-memory-maintenance-validate-operation-{0}.json" -f ([Guid]::NewGuid().ToString("n"))) - Write-JsonFile -Path $operationInputPath -Data $Operation - - try { - $script = @" -const { readFileSync } = require('node:fs'); -const core = require('./packages/core/dist'); -const operation = JSON.parse(readFileSync(process.argv[1], 'utf8')); -core.voidSelfStateOperationSchema.parse(operation); -console.log(JSON.stringify({ ok: true })); -"@ - $output = & node -e $script $operationInputPath 2>&1 - if ($LASTEXITCODE -ne 0) { - throw "Typed operation failed schema validation: $output" - } - } finally { - Remove-Item -LiteralPath $operationInputPath -Force -ErrorAction SilentlyContinue - } -} - -function Convert-ToOperationArray { - param($Value) - - if ($null -eq $Value) { - return @() - } - - if ($Value -is [System.Array]) { - return @($Value) - } - - return @($Value) -} - -function Test-LockProcessAlive { - param($LockRecord) - - $lockPid = Get-ObjectPropertyValue -Value $LockRecord -Name "pid" - if ($null -eq $lockPid) { - return $false - } - - try { - $process = Get-Process -Id ([int]$lockPid) -ErrorAction Stop - return $null -ne $process - } catch { - return $false - } -} - -function Test-LockWithinRuntimeLimit { - param( - $LockRecord, - [Parameter(Mandatory = $true)] - [int] $MaxRuntimeMinutes - ) - - $startedAtRaw = Get-ObjectPropertyString -Value $LockRecord -Name "startedAt" - if ([string]::IsNullOrWhiteSpace($startedAtRaw)) { - return $false - } - - try { - $startedAt = [DateTime]::Parse($startedAtRaw).ToUniversalTime() - } catch { - return $false - } - - return (([DateTime]::UtcNow - $startedAt).TotalMinutes -lt $MaxRuntimeMinutes) -} - -function Test-ActiveLock { - param( - [Parameter(Mandatory = $true)] - [string] $Path, - [Parameter(Mandatory = $true)] - [int] $MaxRuntimeMinutes - ) - - if (-not (Test-Path $Path)) { - return $false - } - - $lockRecord = Read-JsonFile -Path $Path - return (Test-LockProcessAlive -LockRecord $lockRecord) -and - (Test-LockWithinRuntimeLimit -LockRecord $lockRecord -MaxRuntimeMinutes $MaxRuntimeMinutes) -} - -function Assert-AllowedMemoryMaintenanceOperation { - param($Operation) - - $operationName = Get-ObjectPropertyString -Value $Operation -Name "operation" - $allowed = @( - "merge_incubation_support", - "queue_candidate_intervention", - "retire_candidate_intervention", - "upsert_agency_pressure", - "retire_agency_pressure", - "upsert_affect_need", - "retire_affect_need", - "upsert_social_bond", - "retire_social_bond", - "upsert_status_read", - "retire_status_read", - "update_mood_dimensions", - "propose_memory_distillation", - "apply_memory_distillation", - "revise_durable_memory", - "retire_durable_memory", - "crystallize_memory_into_identity", - "prune_short_term_memories" - ) - - if ($null -eq $operationName -or -not $allowed.Contains($operationName)) { - throw "Memory maintenance proposed disallowed operation '$operationName'." - } -} - -trap { - $finishedAtUtc = [DateTime]::UtcNow - $failureMessage = $_.Exception.Message - - Write-JsonFile -Path $statusPath -Data @{ - status = "failed" - startedAt = $startedAtUtc.ToString("o") - finishedAt = $finishedAtUtc.ToString("o") - durationSeconds = [Math]::Round(($finishedAtUtc - $startedAtUtc).TotalSeconds, 2) - failureMessage = $failureMessage - skipModel = [bool]$SkipModel - stateFile = $stateFilePath - contextPath = $contextPath - operationOutputPath = $operationOutputPath - } - - Remove-Item -Path $lockPath -Force -ErrorAction SilentlyContinue - throw -} - -New-Item -ItemType Directory -Force -Path $statusDir, $logDir, (Split-Path -Parent $stateFilePath) | Out-Null - -$maxRuntimeMinutes = if (-not [string]::IsNullOrWhiteSpace($env:VOID_MEMORY_MAINTENANCE_MAX_RUNTIME_MINUTES)) { - [int]$env:VOID_MEMORY_MAINTENANCE_MAX_RUNTIME_MINUTES -} else { - 15 -} - -if (Test-Path $moderationLockPath) { - $moderationLock = Read-JsonFile -Path $moderationLockPath - $moderationProcessAlive = Test-LockProcessAlive -LockRecord $moderationLock - if ($moderationProcessAlive) { - Write-JsonFile -Path $statusPath -Data @{ - status = "skipped" - reason = if (Test-LockWithinRuntimeLimit -LockRecord $moderationLock -MaxRuntimeMinutes $maxRuntimeMinutes) { "moderation_loop_active" } else { "moderation_loop_over_runtime_active" } - observedAt = ([DateTime]::UtcNow.ToString("o")) - stateFile = $stateFilePath - moderationLockPath = $moderationLockPath - maxRuntimeMinutes = $maxRuntimeMinutes - pid = Get-ObjectPropertyValue -Value $moderationLock -Name "pid" - startedAt = Get-ObjectPropertyString -Value $moderationLock -Name "startedAt" - } - return - } - Remove-Item -LiteralPath $moderationLockPath -Force -} - -if (Test-Path $lockPath) { - $existingLock = Read-JsonFile -Path $lockPath - $lockProcessAlive = Test-LockProcessAlive -LockRecord $existingLock - if ($lockProcessAlive -and (Test-LockWithinRuntimeLimit -LockRecord $existingLock -MaxRuntimeMinutes $maxRuntimeMinutes)) { - Write-JsonFile -Path $statusPath -Data @{ - status = "skipped" - reason = "lock_present" - observedAt = ([DateTime]::UtcNow.ToString("o")) - stateFile = $stateFilePath - } - return - } - if ($lockProcessAlive) { - Write-JsonFile -Path $statusPath -Data @{ - status = "skipped" - reason = "lock_over_runtime_active" - observedAt = ([DateTime]::UtcNow.ToString("o")) - stateFile = $stateFilePath - lockPath = $lockPath - maxRuntimeMinutes = $maxRuntimeMinutes - pid = Get-ObjectPropertyValue -Value $existingLock -Name "pid" - startedAt = Get-ObjectPropertyString -Value $existingLock -Name "startedAt" - } - return - } - Remove-Item -LiteralPath $lockPath -Force -} - -Write-JsonFile -Path $lockPath -Data @{ - pid = $PID - startedAt = $startedAtUtc.ToString("o") -} -Write-JsonFile -Path $statusPath -Data @{ - status = "starting" - startedAt = $startedAtUtc.ToString("o") - stateFile = $stateFilePath - lockPath = $lockPath - pid = $PID -} - -if (Test-Path $operationOutputPath) { - Remove-Item -LiteralPath $operationOutputPath -Force -} - -foreach ($requiredPath in @($contextProjectionScriptPath, $selfStateScriptPath, $promptTemplatePath)) { - if (-not (Test-Path $requiredPath)) { - throw "Missing required helper at $requiredPath" - } -} - -$envValues = Read-DotEnv -Path (Join-Path $repoRoot ".env") -$codexExecutable = if (-not [string]::IsNullOrWhiteSpace($env:CODEX_EXECUTABLE)) { - $env:CODEX_EXECUTABLE -} elseif ($envValues.ContainsKey("CODEX_EXECUTABLE") -and -not [string]::IsNullOrWhiteSpace($envValues["CODEX_EXECUTABLE"])) { - $envValues["CODEX_EXECUTABLE"] -} else { - "codex" -} -$codexModel = if (-not [string]::IsNullOrWhiteSpace($env:CODEX_MODEL)) { - $env:CODEX_MODEL -} elseif ($envValues.ContainsKey("CODEX_MODEL") -and -not [string]::IsNullOrWhiteSpace($envValues["CODEX_MODEL"])) { - $envValues["CODEX_MODEL"] -} else { - "gpt-5.4" -} -$codexReasoningEffort = if (-not [string]::IsNullOrWhiteSpace($env:CODEX_MODEL_REASONING_EFFORT)) { - $env:CODEX_MODEL_REASONING_EFFORT -} elseif ($envValues.ContainsKey("CODEX_MODEL_REASONING_EFFORT") -and -not [string]::IsNullOrWhiteSpace($envValues["CODEX_MODEL_REASONING_EFFORT"])) { - $envValues["CODEX_MODEL_REASONING_EFFORT"] -} else { - "medium" -} -$codexExecArgs = if (-not [string]::IsNullOrWhiteSpace($env:CODEX_EXEC_ARGS)) { - Split-CommandArgs -Value $env:CODEX_EXEC_ARGS -} elseif ($envValues.ContainsKey("CODEX_EXEC_ARGS")) { - Split-CommandArgs -Value $envValues["CODEX_EXEC_ARGS"] -} else { - @() -} -$codexTimeoutSeconds = if (-not [string]::IsNullOrWhiteSpace($env:VOID_MEMORY_MAINTENANCE_CODEX_TIMEOUT_SECONDS)) { - [int]$env:VOID_MEMORY_MAINTENANCE_CODEX_TIMEOUT_SECONDS -} elseif ($envValues.ContainsKey("VOID_MEMORY_MAINTENANCE_CODEX_TIMEOUT_SECONDS") -and -not [string]::IsNullOrWhiteSpace($envValues["VOID_MEMORY_MAINTENANCE_CODEX_TIMEOUT_SECONDS"])) { - [int]$envValues["VOID_MEMORY_MAINTENANCE_CODEX_TIMEOUT_SECONDS"] -} else { - [Math]::Max(60, $maxRuntimeMinutes * 60) -} - -$typedContext = Get-TypedSelfState -$typedState = $typedContext.typedState -$isSleepMaintenance = [bool]$ForceDistillation -or [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 { - $status = Get-ObjectPropertyString -Value $_ -Name "status" - $status -eq "queued" -or $status -eq "deferred" - } -).Count -$maintenancePressure = $shortTermMemoryCount + $incubationCount + $activeAgencyPressureCount + $activeCandidateCount - -Write-JsonFile -Path $contextPath -Data @{ - generated = "now" - stateFile = $stateFilePath - selfStateSummary = $typedContext.summary - mode = if ($isSleepMaintenance) { "sleep_maintenance" } else { "awake_memory_maintenance" } - maintenanceBoundary = "This pass may propose only typed memory/incubation/candidate operations. Exact timestamps stay in typed state; prompt-facing chronology uses relative phrases." - sleepDirective = @{ - forceDistillation = [bool]($isSleepMaintenance -and $maintenancePressure -gt 0) - maintenancePressure = [int]$maintenancePressure - 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) -} - -$prompt = Read-TextFile -Path $promptTemplatePath -$prompt = $prompt.Replace("{{CONTEXT_PATH}}", $contextPath) -$prompt = $prompt.Replace("{{STATE_FILE_PATH}}", $stateFilePath) -$prompt = $prompt.Replace("{{OPERATION_OUTPUT_PATH}}", $operationOutputPath) - -$exitCode = 0 -$combinedText = "" - -Write-JsonFile -Path $statusPath -Data @{ - status = if ($SkipModel) { "running_skip_model" } else { "running" } - startedAt = $startedAtUtc.ToString("o") - skipModel = [bool]$SkipModel - forceDistillation = [bool]$ForceDistillation - stateFile = $stateFilePath - contextPath = $contextPath - operationOutputPath = $operationOutputPath - codexExecutable = $codexExecutable - codexModel = $codexModel -} - -if ($SkipModel) { - Append-RunLog "skip-model: writing empty memory-maintenance proposal." - Write-JsonFile -Path $operationOutputPath -Data @() - $combinedText = "Skipped model memory maintenance by explicit -SkipModel smoke mode." -} else { - $codexArgs = @() - $codexArgs += $codexExecArgs - $codexArgs += @( - "exec", - "--ignore-user-config", - "-m", $codexModel, - "-c", 'approval_policy="never"', - "-c", ("model_reasoning_effort={0}" -f (ConvertTo-Json $codexReasoningEffort -Compress)), - "--dangerously-bypass-approvals-and-sandbox", - "--skip-git-repo-check", - "--json", - "-o", $lastMessagePath, - "-" - ) - - $execution = Invoke-CodexExec -Executable $codexExecutable -Arguments $codexArgs -WorkingDirectory $repoRoot -InputText $prompt -TimeoutSeconds $codexTimeoutSeconds - $exitCode = $execution.ExitCode - $combinedText = (($execution.StdOut, $execution.StdErr) | Where-Object { -not [string]::IsNullOrWhiteSpace($_) }) -join [Environment]::NewLine -} - -if ($exitCode -ne 0) { - throw $(if ([string]::IsNullOrWhiteSpace($combinedText)) { "Codex memory maintenance failed with exit code $exitCode." } else { $combinedText }) -} - -if (-not (Test-Path $operationOutputPath)) { - throw "Memory maintenance completed without writing operation output at $operationOutputPath." -} - -$proposedOperations = @(Convert-ToOperationArray -Value (Read-JsonFile -Path $operationOutputPath)) -$appliedOperations = @() - -if ((-not $SkipModel) -and $isSleepMaintenance -and $maintenancePressure -gt 0 -and @($proposedOperations).Count -eq 0) { - throw "Sleep memory maintenance returned no operations despite maintenance pressure $maintenancePressure." -} - -foreach ($operation in $proposedOperations) { - Assert-AllowedMemoryMaintenanceOperation -Operation $operation - Assert-TypedOperationSchema -Operation $operation -} - -foreach ($operation in $proposedOperations) { - $appliedOperations += Apply-TypedOperation -Operation $operation -} - -if ((-not $SkipModel) -and $isSleepMaintenance) { - $refreshedTypedState = Get-TypedSelfState - $remainingShortTermCount = @(Convert-ToValueArray -Value $refreshedTypedState.typedState.thoughtMemory.shortTerm).Count - if ($remainingShortTermCount -gt 0) { - throw "Sleep memory maintenance left $remainingShortTermCount short-term memories unpromoted." - } -} - -$finishedAtUtc = [DateTime]::UtcNow -$lastMessage = if (Test-Path $lastMessagePath) { Get-Content -Path $lastMessagePath -Raw -Encoding UTF8 } else { "" } - -Write-JsonFile -Path $statusPath -Data @{ - status = "ok" - mode = if ($SkipModel) { "typed_memory_maintenance_skip_model" } else { "typed_memory_maintenance" } - startedAt = $startedAtUtc.ToString("o") - finishedAt = $finishedAtUtc.ToString("o") - durationSeconds = [Math]::Round(($finishedAtUtc - $startedAtUtc).TotalSeconds, 2) - exitCode = [int]$exitCode - skipModel = [bool]$SkipModel - forceDistillation = [bool]$ForceDistillation - stateFile = [string]$stateFilePath - contextPath = [string]$contextPath - operationOutputPath = [string]$operationOutputPath - proposedOperationCount = [int]@($proposedOperations).Count - appliedOperationCount = [int]@($appliedOperations).Count - stateUpdated = [bool](@($appliedOperations).Count -gt 0) - lastMessagePath = [string]$lastMessagePath -} - -Append-RunLog ("mode=typed_memory_maintenance proposed={0} applied={1}" -f @($proposedOperations).Count, @($appliedOperations).Count) -Remove-Item -Path $lockPath -Force -ErrorAction SilentlyContinue diff --git a/scripts/run-void-mood-drift.ps1 b/scripts/run-void-mood-drift.ps1 deleted file mode 100644 index 1baa108..0000000 --- a/scripts/run-void-mood-drift.ps1 +++ /dev/null @@ -1,70 +0,0 @@ -Set-StrictMode -Version Latest -$ErrorActionPreference = "Stop" -$ProgressPreference = "SilentlyContinue" - -$repoRoot = Split-Path -Parent $PSScriptRoot -$statusDir = Join-Path $repoRoot ".voidbot\status" -$logDir = Join-Path $repoRoot ".voidbot\logs" -$statusPath = Join-Path $statusDir "void-mood-drift-run.json" -$logPath = Join-Path $logDir "void-mood-drift.log" -$scriptPath = Join-Path $PSScriptRoot "simulate-void-mood.mjs" - -function Write-JsonFile { - param( - [Parameter(Mandatory = $true)] - [string] $Path, - [Parameter(Mandatory = $true)] - $Data - ) - - $directory = Split-Path -Parent $Path - - if (-not [string]::IsNullOrWhiteSpace($directory)) { - New-Item -ItemType Directory -Force -Path $directory | Out-Null - } - - $json = $Data | ConvertTo-Json -Depth 10 - $utf8NoBom = [System.Text.UTF8Encoding]::new($false) - [System.IO.File]::WriteAllText($Path, $json, $utf8NoBom) -} - -function Append-RunLog { - param( - [Parameter(Mandatory = $true)] - [string] $Line - ) - - $timestamped = "[{0}] {1}" -f ([DateTime]::UtcNow.ToString("o")), $Line - Add-Content -Path $logPath -Value $timestamped -Encoding UTF8 -} - -New-Item -ItemType Directory -Force -Path $statusDir, $logDir | Out-Null - -$startedAt = [DateTime]::UtcNow - -Write-JsonFile -Path $statusPath -Data @{ - status = "running" - startedAt = $startedAt.ToString("o") - scriptPath = $scriptPath -} - -$output = & node $scriptPath 2>&1 -$exitCode = $LASTEXITCODE -$finishedAt = [DateTime]::UtcNow -$combined = ($output | Out-String).Trim() - -Write-JsonFile -Path $statusPath -Data @{ - status = if ($exitCode -eq 0) { "ok" } else { "failed" } - startedAt = $startedAt.ToString("o") - finishedAt = $finishedAt.ToString("o") - durationSeconds = [Math]::Round(($finishedAt - $startedAt).TotalSeconds, 2) - exitCode = $exitCode - output = $combined - scriptPath = $scriptPath -} - -Append-RunLog ("exit={0} output={1}" -f $exitCode, $combined) - -if ($exitCode -ne 0) { - throw "Void mood drift failed with exit code $exitCode. $combined" -} diff --git a/scripts/simulate-void-mood.mjs b/scripts/simulate-void-mood.mjs deleted file mode 100644 index 74ad20f..0000000 --- a/scripts/simulate-void-mood.mjs +++ /dev/null @@ -1,400 +0,0 @@ -import { spawnSync } from "node:child_process"; -import { closeSync, mkdirSync, openSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs"; -import { createRequire } from "node:module"; -import { dirname, resolve } from "node:path"; -import { fileURLToPath } from "node:url"; - -const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), ".."); -const require = createRequire(import.meta.url); -const { - applyVoidSelfStateOperation, - loadVoidSelfStateTypedDocuments, -} = require(resolve(repoRoot, "packages/core/dist/index.js")); - -const rawArgs = process.argv.slice(2); -const args = new Set(rawArgs); -const statePath = resolveArgPath("--state-path", ".voidbot/private/void-self-state.cc"); -const statusDir = resolveStatusDir(); -const statusPath = resolve(statusDir, "void-mood-drift.json"); -const lockPath = resolve(statusDir, "void-mood-drift.lock"); -const lastSpeechPath = resolve(statusDir, "void-last-speech.json"); -const moderationLockPath = resolve(statusDir, "moderation-rumination.lock"); -const memoryMaintenanceStatusPath = resolveMemoryMaintenanceStatusPath(); -const memoryMaintenanceScriptPath = resolve(repoRoot, "scripts/run-void-memory-maintenance.ps1"); - -async function main() { - const now = new Date(); - - await withLock(async () => { - const typedState = await loadVoidSelfStateTypedDocuments({ canonicalPath: statePath }); - - if (isRecentLockPresent(moderationLockPath, 20 * 60 * 1000)) { - const speakingPressure = updateSpeakingPressure({ - typedState, - previous: typedState.scheduledRuntime.speakingPressure, - sleepCycle: typedState.scheduledRuntime.sleepCycle, - now, - }); - await applyVoidSelfStateOperation( - { canonicalPath: statePath }, - { - operation: "update_speaking_pressure", - speakingPressure, - }, - ); - writeStatus({ - status: "skipped", - reason: "moderation_loop_active", - observedAt: now.toISOString(), - statePath, - speakingPressure, - sleepCycle: typedState.scheduledRuntime.sleepCycle, - }); - return; - } - - const sleepCycle = updateSleepCycle(typedState.scheduledRuntime.sleepCycle, now); - const speakingPressure = updateSpeakingPressure({ - typedState, - previous: typedState.scheduledRuntime.speakingPressure, - sleepCycle, - now, - }); - - await applyVoidSelfStateOperation( - { canonicalPath: statePath }, - { - operation: "update_sleep_cycle", - sleepCycle, - }, - ); - await applyVoidSelfStateOperation( - { canonicalPath: statePath }, - { - operation: "update_speaking_pressure", - speakingPressure, - }, - ); - - const memoryMaintenance = runSleepMemoryMaintenanceIfDue({ sleepCycle, typedState }); - if (memoryMaintenance.status === "failed") { - writeStatus({ - status: "failed", - reason: "sleep_memory_maintenance_failed", - observedAt: now.toISOString(), - statePath, - speakingPressure, - sleepCycle, - memoryMaintenance, - }); - throw new Error(`Sleep memory maintenance failed with exit code ${memoryMaintenance.exitCode}.`); - } - - writeStatus({ - status: "ok", - observedAt: now.toISOString(), - statePath, - speakingPressure, - sleepCycle, - memoryMaintenance, - }); - }); -} - -function updateSleepCycle(previous, now) { - const prior = previous && typeof previous === "object" ? previous : {}; - const napIntervalMs = 4 * 60 * 60 * 1000; - const napDurationMs = 60 * 60 * 1000; - const nowMs = now.getTime(); - let isNapping = prior.isNapping === true; - let currentNapStartedAt = typeof prior.currentNapStartedAt === "string" ? prior.currentNapStartedAt : undefined; - let currentNapEndsAt = typeof prior.currentNapEndsAt === "string" ? prior.currentNapEndsAt : undefined; - let nextNapStartsAt = typeof prior.nextNapStartsAt === "string" ? prior.nextNapStartsAt : undefined; - let nextNapMs = nextNapStartsAt ? Date.parse(nextNapStartsAt) : Number.NaN; - let napEndMs = currentNapEndsAt ? Date.parse(currentNapEndsAt) : Number.NaN; - - if (!Number.isFinite(nextNapMs)) { - nextNapMs = nowMs + napIntervalMs; - nextNapStartsAt = new Date(nextNapMs).toISOString(); - } - - if (isNapping && Number.isFinite(napEndMs) && nowMs >= napEndMs) { - isNapping = false; - currentNapStartedAt = undefined; - currentNapEndsAt = undefined; - nextNapMs = nowMs + napIntervalMs; - nextNapStartsAt = new Date(nextNapMs).toISOString(); - } - - if (!isNapping && nowMs >= nextNapMs) { - isNapping = true; - currentNapStartedAt = now.toISOString(); - currentNapEndsAt = new Date(nowMs + napDurationMs).toISOString(); - nextNapStartsAt = new Date(nowMs + napIntervalMs).toISOString(); - } - - return { - isNapping, - currentNapStartedAt, - currentNapEndsAt, - nextNapStartsAt, - activeDreamThemes: Array.isArray(prior.activeDreamThemes) - ? prior.activeDreamThemes.filter((value) => typeof value === "string" && value.trim().length > 0).slice(0, 4) - : [], - }; -} - -function updateSpeakingPressure({ typedState, previous, sleepCycle, now }) { - const lastSpeech = readJsonSafe(lastSpeechPath); - const lastSpokeAt = - typeof lastSpeech?.sentAt === "string" - ? lastSpeech.sentAt - : typeof previous.lastSpokeAt === "string" - ? previous.lastSpokeAt - : undefined; - const hoursSinceSpeech = lastSpokeAt ? hoursBetween(now, new Date(lastSpokeAt)) : 24; - const recentSpeechDamping = clamp(Math.exp(-hoursSinceSpeech / 3.5), 0, 1); - 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.3 : 0.14; - const readinessWeight = entry.status === "ready_to_act" ? 1.25 : 1.08; - 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)) - .slice(0, 4) - .reduce((sum, thread) => sum + (thread.desireToSpeak ?? 0) * 0.12, 0); - const affectNeedPressure = (typedState.faceAffect?.needs ?? []) - .filter((entry) => entry.status === "active" || entry.status === "neglected") - .reduce((sum, entry) => { - const neglectedWeight = entry.status === "neglected" ? 1.28 : 1; - const substrateWeight = - entry.kind === "substrate" || entry.kind === "agency" || entry.kind === "status" ? 0.18 : 0.12; - return sum + entry.intensity * substrateWeight * neglectedWeight; - }, 0); - const statusReadPressure = (typedState.faceAffect?.statusReads ?? []) - .filter((entry) => !entry.retiredAt) - .reduce((sum, entry) => { - const sharpStatusWeight = ["neglected", "bypassed", "blocked", "ignored", "threatened"].includes(entry.status) - ? 0.14 - : 0.08; - return sum + entry.intensity * sharpStatusWeight; - }, 0); - const moodSpeechPressure = (typedState.faceAffect?.moodDimensions ?? []) - .reduce((sum, entry) => { - const expressiveWeight = ["anger", "annoyance", "irritation", "envy", "pride", "smugness", "playfulness", "anxiety", "commandForce"].includes(entry.name) - ? 0.06 - : 0.025; - return sum + entry.value * expressiveWeight; - }, 0); - const silencePressure = clamp(hoursSinceSpeech / 6, 0, 1); - const sleepiness = sleepCycle.isNapping ? 1 : 0; - const priorNeedToSpeak = clamp(previous.needToSpeak ?? 0.35, 0, 1); - const targetNeedToSpeak = clamp( - 0.18 + silencePressure * 0.32 + queuedInterventionPressure + topThreadPressure + agencySpeechPressure + affectNeedPressure + statusReadPressure + moodSpeechPressure - recentSpeechDamping * 0.38 - sleepiness * 0.26, - 0, - 1, - ); - const needToSpeak = round3(priorNeedToSpeak + (targetNeedToSpeak - priorNeedToSpeak) * 0.42); - const noveltyPressure = round3( - clamp((previous.noveltyPressure ?? 0.35) * 0.6 + topThreadPressure * 0.72 + queuedInterventionPressure * 0.3 + agencySpeechPressure * 0.44 + affectNeedPressure * 0.38 + statusReadPressure * 0.3, 0, 1), - ); - const confessionPressure = round3( - clamp((previous.confessionPressure ?? 0.25) * 0.68 + silencePressure * 0.12 + sleepiness * 0.1 + moodSpeechPressure * 0.4, 0, 1), - ); - - return { - needToSpeak, - confessionPressure, - noveltyPressure, - recentSpeechDamping: round3(recentSpeechDamping), - lastSpokeAt, - }; -} - -function readJsonSafe(path) { - try { - return JSON.parse(readFileSync(path, "utf8")); - } catch { - return null; - } -} - -function runSleepMemoryMaintenanceIfDue({ sleepCycle, typedState }) { - if (args.has("--skip-memory-maintenance")) { - return { - status: "skipped", - reason: "disabled_by_argument", - }; - } - - if (sleepCycle.isNapping !== true) { - return { - status: "skipped", - reason: "not_napping", - }; - } - - const napStartedMs = Date.parse(sleepCycle.currentNapStartedAt ?? ""); - if (!Number.isFinite(napStartedMs)) { - return { - status: "skipped", - reason: "missing_nap_start", - }; - } - - if (!args.has("--force-memory-maintenance") && completedMemoryMaintenanceAfter(napStartedMs)) { - return { - status: "skipped", - reason: "already_completed_this_nap", - }; - } - - const shortTermPressure = (typedState.thoughtMemory?.shortTerm ?? []) - .filter((memory) => typeof memory?.retiredAt !== "string" || memory.retiredAt.trim().length === 0) - .length; - if (!args.has("--force-memory-maintenance") && shortTermPressure === 0) { - return { - status: "skipped", - reason: "no_short_term_memory_pressure", - }; - } - - const result = spawnSync( - "powershell", - [ - "-NoProfile", - "-ExecutionPolicy", - "Bypass", - "-File", - memoryMaintenanceScriptPath, - "-StateFilePath", - statePath, - ], - { - cwd: repoRoot, - encoding: "utf8", - windowsHide: true, - }, - ); - - const output = [result.stdout, result.stderr].filter((value) => value && value.trim().length > 0).join("\n").trim(); - if (result.status !== 0) { - return { - status: "failed", - exitCode: result.status, - output, - }; - } - - return { - status: "ok", - exitCode: result.status, - output, - }; -} - -function completedMemoryMaintenanceAfter(timestampMs) { - const status = readJsonSafe(memoryMaintenanceStatusPath); - if (status?.status !== "ok") { - return false; - } - if (typeof status.stateFile !== "string" || resolve(status.stateFile) !== statePath) { - return false; - } - - const finishedMs = Date.parse(status.finishedAt ?? ""); - return Number.isFinite(finishedMs) && finishedMs >= timestampMs; -} - -function writeStatus(payload) { - mkdirSync(dirname(statusPath), { recursive: true }); - writeFileSync(statusPath, `${JSON.stringify(payload, null, 2)}\n`, "utf8"); -} - -async function withLock(fn) { - mkdirSync(dirname(lockPath), { recursive: true }); - let fd; - try { - fd = openSync(lockPath, "wx"); - } catch (error) { - if (error?.code === "EEXIST" && isRecentLockPresent(lockPath, 10 * 60 * 1000)) { - writeStatus({ - status: "skipped", - reason: "lock_present", - observedAt: new Date().toISOString(), - }); - return; - } - rmSync(lockPath, { force: true }); - fd = openSync(lockPath, "wx"); - } - - try { - writeFileSync(fd, `${process.pid}\n${new Date().toISOString()}\n`, "utf8"); - await fn(); - } finally { - if (typeof fd === "number") { - closeSync(fd); - } - rmSync(lockPath, { force: true }); - } -} - -function isRecentLockPresent(path, thresholdMs) { - try { - const stats = statSync(path); - return Date.now() - stats.mtimeMs < thresholdMs; - } catch { - return false; - } -} - -function hoursBetween(later, earlier) { - const diff = later.getTime() - earlier.getTime(); - return Number.isFinite(diff) ? Math.max(0, diff / (60 * 60 * 1000)) : 24; -} - -function round3(value) { - return Math.round(value * 1000) / 1000; -} - -function clamp(value, min, max) { - if (!Number.isFinite(value)) { - return min; - } - return Math.min(max, Math.max(min, value)); -} - -await main(); - -function getArgValue(name) { - const index = rawArgs.indexOf(name); - if (index < 0) { - return undefined; - } - - const value = rawArgs[index + 1]; - return typeof value === "string" && !value.startsWith("--") ? value : undefined; -} - -function resolveArgPath(name, fallbackRelativePath) { - const value = getArgValue(name); - return value ? resolve(value) : resolve(repoRoot, fallbackRelativePath); -} - -function resolveStatusDir() { - const value = process.env.VOID_STATUS_DIR; - return resolve(value && value.trim().length > 0 ? value : resolve(repoRoot, ".voidbot/status")); -} - -function resolveMemoryMaintenanceStatusPath() { - const statusDir = process.env.VOID_MEMORY_MAINTENANCE_STATUS_DIR; - return resolve(statusDir && statusDir.trim().length > 0 ? statusDir : resolveStatusDir(), "void-memory-maintenance.json"); -} diff --git a/scripts/smoke-void-agency-pressure-fixture.ps1 b/scripts/smoke-void-agency-pressure-fixture.ps1 deleted file mode 100644 index fca888f..0000000 --- a/scripts/smoke-void-agency-pressure-fixture.ps1 +++ /dev/null @@ -1,85 +0,0 @@ -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 - 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" - 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/scripts/smoke-void-memory-maintenance-sleep-fixture.ps1 b/scripts/smoke-void-memory-maintenance-sleep-fixture.ps1 deleted file mode 100644 index 4a19f4a..0000000 --- a/scripts/smoke-void-memory-maintenance-sleep-fixture.ps1 +++ /dev/null @@ -1,183 +0,0 @@ -Set-StrictMode -Version Latest -$ErrorActionPreference = "Stop" -$ProgressPreference = "SilentlyContinue" - -$repoRoot = Split-Path -Parent $PSScriptRoot -Set-Location -LiteralPath $repoRoot - -$tempRoot = Join-Path $env:TEMP ("void-memory-maintenance-fixture-" + [guid]::NewGuid().ToString("n")) -$stateFilePath = Join-Path $tempRoot "void-self-state.cc" -$fakeCodexPath = Join-Path $tempRoot "fake-codex-memory-maintenance.mjs" -$recordOperationPath = Join-Path $tempRoot "record-short-term.json" -$sleepOperationPath = Join-Path $tempRoot "sleep-cycle.json" -$fixtureStatusDir = Join-Path $tempRoot "status" -$fixtureLogDir = Join-Path $tempRoot "logs" -$statusOperationPath = Join-Path $fixtureStatusDir "void-memory-maintenance-operations.json" - -New-Item -ItemType Directory -Force -Path $tempRoot | Out-Null - -try { - $fakeCodexSource = @' -import { mkdirSync, writeFileSync } from "node:fs"; -import { dirname, resolve } from "node:path"; - -const operationOutputPath = resolve(process.env.VOID_MEMORY_FIXTURE_OPERATION_OUTPUT); -const lastMessageIndex = process.argv.indexOf("-o"); -const lastMessagePath = lastMessageIndex >= 0 ? process.argv[lastMessageIndex + 1] : undefined; - -let prompt = ""; -process.stdin.setEncoding("utf8"); -process.stdin.on("data", (chunk) => { - prompt += chunk; -}); -process.stdin.on("end", () => { - if (!prompt.includes("Sleep forces distillation")) { - console.error("fixture prompt did not contain the sleep distillation contract"); - process.exit(2); - } - - const operation = { - operation: "apply_memory_distillation", - proposalId: "fixture-sleep-distillation", - sourceMemoryIds: ["fixture-short-term-memory"], - memory: { - memoryId: "fixture-durable-memory", - kind: "project_seam", - target: { - kind: "repo", - id: "AquariumSynthCSharp", - label: "AquariumSynthCSharp", - }, - summary: "Workflow cannot own the body.", - 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.", - 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: [], - }, - appliedAt: "2026-05-17T01:00:00.000Z", - }; - - mkdirSync(dirname(operationOutputPath), { recursive: true }); - writeFileSync(operationOutputPath, `${JSON.stringify([operation], null, 2)}\n`, "utf8"); - - if (lastMessagePath) { - mkdirSync(dirname(resolve(lastMessagePath)), { recursive: true }); - writeFileSync(lastMessagePath, "fixture memory maintenance wrote one sleep distillation operation\n", "utf8"); - } - - process.stdout.write(JSON.stringify({ type: "fixture", operationCount: 1 }) + "\n"); -}); -'@ - [System.IO.File]::WriteAllText($fakeCodexPath, $fakeCodexSource, [System.Text.UTF8Encoding]::new($false)) - - $recordOperation = @{ - operation = "record_short_term_memory" - memory = @{ - memoryId = "fixture-short-term-memory" - kind = "project_seam" - target = @{ - kind = "repo" - id = "AquariumSynthCSharp" - label = "AquariumSynthCSharp" - } - summary = "Workflow cannot own the body." - 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." - 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 = @() - } - } - $sleepOperation = @{ - operation = "update_sleep_cycle" - sleepCycle = @{ - isNapping = $true - currentNapStartedAt = "2026-05-17T00:00:00.000Z" - currentNapEndsAt = "2099-05-17T01:00:00.000Z" - nextNapStartsAt = "2099-05-17T04:00:00.000Z" - activeDreamThemes = @("fixture-distillation") - } - } - - $recordOperation | ConvertTo-Json -Depth 32 | Set-Content -LiteralPath $recordOperationPath -Encoding UTF8 - $sleepOperation | ConvertTo-Json -Depth 32 | Set-Content -LiteralPath $sleepOperationPath -Encoding UTF8 - - node .\scripts\void-self-state.mjs apply-operation --canonical $stateFilePath --operation-file $recordOperationPath | Out-Null - node .\scripts\void-self-state.mjs apply-operation --canonical $stateFilePath --operation-file $sleepOperationPath | Out-Null - - $previousCodexExecutable = $env:CODEX_EXECUTABLE - $previousCodexExecArgs = $env:CODEX_EXEC_ARGS - $previousFixtureOutput = $env:VOID_MEMORY_FIXTURE_OPERATION_OUTPUT - $previousStatusDir = $env:VOID_STATUS_DIR - $previousMaintenanceStatusDir = $env:VOID_MEMORY_MAINTENANCE_STATUS_DIR - $previousMaintenanceLogDir = $env:VOID_MEMORY_MAINTENANCE_LOG_DIR - - try { - $env:CODEX_EXECUTABLE = "node" - $env:CODEX_EXEC_ARGS = $fakeCodexPath - $env:VOID_MEMORY_FIXTURE_OPERATION_OUTPUT = $statusOperationPath - $env:VOID_STATUS_DIR = $fixtureStatusDir - $env:VOID_MEMORY_MAINTENANCE_STATUS_DIR = $fixtureStatusDir - $env:VOID_MEMORY_MAINTENANCE_LOG_DIR = $fixtureLogDir - - node .\scripts\simulate-void-mood.mjs --state-path $stateFilePath --force-memory-maintenance | Out-Null - } finally { - $env:CODEX_EXECUTABLE = $previousCodexExecutable - $env:CODEX_EXEC_ARGS = $previousCodexExecArgs - $env:VOID_MEMORY_FIXTURE_OPERATION_OUTPUT = $previousFixtureOutput - $env:VOID_STATUS_DIR = $previousStatusDir - $env:VOID_MEMORY_MAINTENANCE_STATUS_DIR = $previousMaintenanceStatusDir - $env:VOID_MEMORY_MAINTENANCE_LOG_DIR = $previousMaintenanceLogDir - } - - $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 - $shortTermCount = @($state.thoughtMemory.shortTerm).Count - $durableCount = @($state.thoughtMemory.memories).Count - - if ($shortTermCount -ne 0) { - throw "Sleep fixture left $shortTermCount short-term memories behind." - } - if ($durableCount -ne 1) { - throw "Sleep fixture expected one durable memory, found $durableCount." - } - - $durableMemory = @($state.thoughtMemory.memories)[0] - if ($durableMemory.summary -ne "Workflow cannot own the body.") { - throw "Sleep fixture distorted the memory summary." - } - 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.anchorRefs).Count -lt 1) { - throw "Sleep fixture produced a durable memory without anchor refs." - } - - @{ - status = "ok" - shortTermCount = $shortTermCount - durableCount = $durableCount - durableSummary = $durableMemory.summary - } | ConvertTo-Json -Compress -} finally { - Remove-Item -LiteralPath $tempRoot -Recurse -Force -ErrorAction SilentlyContinue -} diff --git a/state/evidence.jsonl b/state/evidence.jsonl index de1230d..25be469 100644 --- a/state/evidence.jsonl +++ b/state/evidence.jsonl @@ -451,3 +451,4 @@ {"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."} {"ts":"2026-07-16T04:58:31Z","type":"architecture","status":"ok","note":"Cut the obsolete Windows Void rumination authority: deleted the 1,762-line runner, task installer, PowerShell-only rumination/moderation fixtures, CTB system-agent launcher, and dead moderation-lock observers; removed both Void organs from Local Orchestrator and removed legacy config/scripts. Persona initiative now registers only real Persona identities. Consolidated resident-organ smoke and TypeScript build pass, and deployed b09267e proved both model cadences cold before the cut."} +{"ts":"2026-07-16T05:03:14Z","type":"architecture","status":"ok","note":"Cut the remaining local Void sleep/memory authority: deleted mood task/simulator, PowerShell memory runner, shared shell chronology projector, obsolete prompt, and shell-only fixtures. Resident physiology and nap-deduped memory maintenance are now the only owners. Direct Void Discord mentions now use the normal bot prompt/job lane rather than impossible built-in-CTB attention; live scheduler state had 22 registered Personas, zero Void pending mentions, and zero total pending debt before rollout."} diff --git a/state/map.yaml b/state/map.yaml index 0663e54..0308a6f 100644 --- a/state/map.yaml +++ b/state/map.yaml @@ -116,7 +116,7 @@ accepted_design: - Quiet-room thought sharing should also stop treating "no live hook" as a sacred veto. When a world-facing seam is concrete, ripe, and still reasonably novel, the default question should become how to say it cleanly, and the typed state boundary should preserve a durable candidate share instead of relying on prompt bravery alone. - The moderation loop cannot trust archive polling alone to remember that it already spoke. Recent delivered replies belong in canonical typed speech receipts, and direct-question dedupe should consult those receipts instead of assuming Discord history will reliably echo the bot's own sends back into view. - Once a reviewed cursor exists, chronological moderation polling should stay exact. The unattended child should get one concrete `export-recent-discord-history.mjs --after ` command for the run and should not widen that back into a bootstrap lookback unless there truly is no saved cursor. - - Prompt-facing chronology is a projection, not state. Exact timestamps may live in typed documents, status files, and cursor bookkeeping; resident moderation and rumination use `relative-chronology-projector.ts`, while the remaining legacy memory-maintenance PowerShell path still uses `scripts/lib/void-rumination-context-projection.ps1` until that shell is deleted. + - Prompt-facing chronology is a projection, not state. Exact timestamps may live in typed documents, status files, and cursor bookkeeping; all resident Void cognition organs use `relative-chronology-projector.ts`. The PowerShell chronology projection has been deleted. - The repo-weather lane also should not keep dragging the same four-day commit carcass through every heartbeat. The recent repo-activity helper now advances the typed repo-activity cursor so already-injected commits stop coming back as fresh weather by default. - Repo weather should lead with particulars, not slogans. If two repos only rhyme at bumper-sticker altitude, keep the reports separate instead of forcing a fake unifying law over them. - Public artifacts attributed to Void should be discovered through the ordinary repo-weather content-hint and retrieval path, then presented through typed candidate interventions to the configured public room when Void confirms authorship and recent receipts/candidates do not show the artifact was already shared. @@ -177,12 +177,12 @@ accepted_design: - First chat under a repo identity must initialize the target repo's own `.voidbot` home: `voice/` for identity and mouth files, `state/` for Face/swarm state, `birth/` for Epiphany birth artifacts, and `logs/` for durable worker output. The birth hook uses EpiphanyAgent's repo-personality/birth-runner path over repo contents and git history instead of inventing a VoidBot-only personality generator. - Repo Face personality seeding has a named prompt surface now: `prompts/repo-face-personality-seed.prompt.md`. Use it for newborn or strengthening passes that trawl repo docs, source layout, commit history, repo-local `.voidbot` material, and legacy `memory.json`, then convert useful meaning into typed `.cc` state instead of leaving it in inert memory files. - Recent chat consensus can now be packaged for any target Codex workspace through `scripts/feed-codex-chat-consensus.mjs`. The helper reads archived Discord messages, defaults to Aquarium, writes a markdown consensus packet plus a Codex prompt under `.voidbot/artifacts/chat-consensus/`, and only feeds `codex exec` when `--execute` is explicit. - - Agent standing heartbeats are owned by one CTB-style initiative scheduler, not by one task per agent. Wall-clock elapsed time advances recovery bookkeeping; it does not create turn intent. The scheduled task only supplies frequent observation pulses. `initiative-engine.ts` may admit a turn only for an explicit pending mention or fresh semantic response pressure at or above that participant's interrupt threshold, and it still admits at most one pressure-backed unprompted participant per `REPO_FACE_HEARTBEAT_IDLE_RECOVERY_MINUTES` window. The scheduler stores per-agent speed/load/groups/heat/effective-speed/next-turn/active-turn state at `REPO_FACE_HEARTBEAT_STATE_PATH`, dispatches repo Faces as ordinary `repo-face-rumination` jobs, and dispatches built-in `void` as the existing moderation rumination runner. Global heat is operator-controlled through the `voidbot.swarm` Eve slider: Hermodr publishes `gamecult.eve.command.v1` to the provider-advertised RUDP command route, `serve-voidbot-swarm-cultmesh.cjs` validates `swarm.set_heat`, and `.voidbot/private/swarm-controls.cc` owns the accepted value that the scheduler reads before the environment default. Heat overrides can target participant kind, turn kind, identity, repo, display name, channel, or `all` without creating new schedulers, but heat only changes recovery speed; it cannot manufacture eligibility or increase aggregate unprompted throughput. Pending mentions remain obligations and bypass the global unprompted cadence. Active-turn freeze and rest gating remain hard brakes unless a direct mention creates room debt. An agent's initiative is frozen while its owner-Codex job or moderation lock is active; recovery is scheduled after completion, not when the job is queued. + - Persona standing heartbeats are owned by one CTB-style initiative scheduler, not by one task per agent. Wall-clock elapsed time advances recovery bookkeeping; it does not create turn intent. `initiative-engine.ts` may admit a Persona only for an explicit pending mention or fresh semantic response pressure at or above that participant's interrupt threshold, with one pressure-backed unprompted admission per global window. It dispatches only repo/native Persona jobs. Void moderation and rumination never enter CTB. Global heat is operator-controlled through the `voidbot.swarm` Eve slider and can change Persona recovery speed, not intent or aggregate throughput. - Dynamic Persona response pressure is a derived scheduler input, not Persona memory or a second initiative owner. Each pulse embeds canonical Face identity-card projections and recent permitted-channel human messages through the existing RAG embedder, converts cosine evidence into bounded recency-decayed noisy-OR pressure, caps awakening to the three strongest scheduler-eligible Faces, and records message-scoped evidence plus one-shot semantic interrupt receipts. Pressure changes the recovery calculation when recovery is next scheduled through `dynamicHeat`; only threshold crossing may pull `nextTurnAt` to the current initiative clock. Pending mentions remain hard obligations and active-turn, rest, blocked-status, idle-cooling, and max-job brakes remain authoritative. Embedding failure fails cool at baseline heat. - Runtime liveness is not enough. The operations watchdog must also verify bot/worker freshness by comparing each live process start time against its built `dist/index.js` entrypoint timestamp. A process that predates its entrypoint is stale and must fail `runtime..freshness`; otherwise source/build repairs can silently sit beside an old in-memory worker that keeps eating new Face action DSL output as private summaries. - Active-turn freeze is bounded by actual job liveness, not blind faith in an old `running` row. Repo Face heartbeat jobs that remain approved/running past the stale-job window are marked failed through the job queue, recorded as `stale_active_turn_recovered`, and removed from the active-turn map so one dead worker claim cannot silence a Face indefinitely. - Repo Face rest projection is parent-owned state, not a read-only scheduler illusion. If the heartbeat runner projects a Face sleep cycle forward, it must persist the normalized `update_sleep_cycle` operation to that Face's `.cc` state before ready selection. Missed nap windows roll forward as completed rest instead of starting a brand-new nap at inspection time; otherwise overdue agents can be skipped forever as "currently napping" while raw state keeps the same stale boundary. - - Repo Face memory distillation now shares Void's typed maintenance organ instead of relying on rumination turns to eventually clean themselves. `scripts/run-repo-face-heartbeats.ts` starts repo-local `run-void-memory-maintenance.ps1 -ForceDistillation` when a Face is napping with short-term memory or has stale short-term memory older than six hours, with status/logs isolated under the target repo's `.voidbot/status` and `.voidbot/logs`. `run-void-memory-maintenance.ps1` validates the entire proposed operation batch against `voidSelfStateOperationSchema` before applying anything, so one malformed late operation cannot leave a partial maintenance pass. The maintenance prompt is identity-neutral and explicitly forbids shorthand operation payloads, `type` instead of `operation`, `memoryIds` for pruning, and `null` optional fields. + - Void's memory distillation is owned only by the resident typed maintenance organ. Persona memory maintenance needs its own explicit future owner if required; Persona turns cannot launch Void's deleted PowerShell maintenance body as a compensator. - Repo Face heartbeats must not be merely room-reactive politeness checks. The heartbeat prompt carries a jurisdiction-dive cadence, but it is background appetite, not a command that outranks live conversation. When no correction, direct call, social pressure, or stronger embodied need is tugging, the Face should inspect its repo/history terrain with source tools before making repo claims; when the live room is the real thing, answer the room and preserve the dive as later hunger. - Repo Face heartbeats are not ordinary owner Discord replies. The owner-Codex render wrapper must recognize both legacy `Repo Face identity doctrine` prompts and current `Epiphany Face identity doctrine` heartbeat prompts, forbid generic handoff for Face jobs, and preserve the repo identity action rail as parent-owned output. The preferred rail is now the small Face action DSL; legacy JSON sentinels remain accepted only as a compatibility surface. - Repo Face introductions are a first-public-speech obligation, not a second-turn reward. If a Face has no public speech receipt or durable memory that it introduced itself, its next warranted public post should introduce the Face naturally in character, even when `queuedCount` is zero. @@ -252,23 +252,23 @@ landed_baseline: - Typed moderation heartbeat/enforcement, person-shaped rumination, physiology, memory maintenance, and candidate delivery are resident Yggdrasil daemon organs with independent gates and cadences. The Local Orchestrator no longer owns any of them. - The active self-state service no longer registers, reads, or writes the legacy moderation document during routine operations. - The old memory-organ cleanup/vector-incubation/agency stack, legacy state template, legacy moderation context exporter, legacy moderation-state store, and JSON projection paths are deleted or offline; replacements must be typed-operation native. - - Memory maintenance now has a typed runner boundary: `scripts/run-void-memory-maintenance.ps1` builds prompt-facing typed context, loads `prompts/void-memory-maintenance.md`, rejects non-maintenance operations, and applies allowed memory/incubation/candidate proposals through `scripts/void-self-state.mjs`. - - Sleep is the forcing function for memory distillation. `scripts/simulate-void-mood.mjs` invokes typed memory maintenance once per nap, and a real sleep pass with memory pressure must return at least one operation instead of letting yesterday's rumination residue linger. + - Resident memory maintenance has a typed organ boundary: physiology emits a nap intent; the organ projects bounded context, pre-receipts one attempt, performs one tool-free model call, validates the full operation array, applies it through the typed state service, and verifies no short-term residue remains. + - Sleep is the forcing function for memory distillation. The resident physiology/memory handoff permits one attempt per nap and cannot be recreated by a local mood script. - 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 consolidated resident-organ smoke seeds typed sleep pressure, proves the maintenance intent and one-attempt receipt, applies a bounded fake model operation, and verifies short-term residue is consumed. - The local `GameCult Local Orchestrator` has no Void moderation, rumination, mood, memory, candidate-delivery, or Persona authority. Remaining local Bifrost/watchdog/dashboard duties are separate migration debt and cannot recreate a Void decision loop. - VoidBot stack startup now treats Bifrost's CultMesh command lane as a dependency. `scripts/start-voidbot-stack.ps1` checks `BIFROST_CULTMESH_COMMAND_URI`, `BIFROST_CULTMESH_STORE_PATH`, and the Bifrost command processor, exports the provider store when missing, and records `runtime-stack.json.bifrost` before starting the bot and worker. Product HTTP health is not VoidBot's authority for Bifrost daemon readiness. - `scripts/smoke-persona-initiative-engine.ts` covers the resident rumination model boundary, typed operation allow-list, stable pressure fingerprint, nap brake, advocacy accountability, candidate targeting, Bifrost receipt, and no-double-delivery invariants without invoking the deleted Windows runner. - - The live sleep-maintenance gate has passed once on real state. A manual typed nap triggered `scripts/run-void-memory-maintenance.ps1`, which consumed the scheduled rumination short-term memory with one `apply_memory_distillation` operation and left `thoughtMemory.shortTerm` empty while preserving the durable `identity_seam` target, claim, tension, action implication, and anchors. + - Historical live sleep maintenance consumed one scheduled-rumination short-term memory into a meaning-preserving durable `identity_seam`; current runtime ownership is the resident organ, not the deleted manual runner. - Awake rumination has a daemon-owned sleep brake. If Void is napping without external or internal pressure, `void-rumination-organ.ts` returns before model invocation; the consolidated resident-organ smoke verifies this path. - Short-term rumination now has a parent-side clustering guard. `record_short_term_memory` merges same target/topic pressure into one provisional memory with merged anchors and a stable source id, while `npm run moderation:short-term-clustering:fixture` proves related synth-boundary memories collapse and unrelated same-repo topics stay separate. The guard should preserve deepening by carrying fresh anchors forward; saturation is a judgment about whether the thought is still changing, not a raw duplicate counter. - 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. - The agency-to-speech seam is owned by the resident rumination projector. Strong active self/world advocacy pressure rejects silent output unless it becomes a queued candidate or is explicitly cooled/retired; the consolidated resident-organ smoke proves the failure and queued-candidate paths. - Deferred candidate interventions are not speech. A pressure is not resolved until it becomes a queued/spoken candidate or is explicitly cooled/retired, and the rumination runner now delivers already-queued candidates with delivery targets instead of only delivering candidates proposed in the current pass. - Parent-owned candidate delivery must trust canonical speech receipts more than fresh enthusiasm. If a queued candidate points at a Discord reply target that already has a typed receipt, the runner should retire it before delivery instead of answering the same message twice with a slightly different haircut. Typed state writes also normalize receipts to keep only the earliest canonical receipt per `channelId + replyToMessageId`. - - Speaking-pressure freshness must not depend on a full mood-drift window. Mood drift now refreshes `scheduledRuntime.speakingPressure` from canonical speech receipts even when it skips deeper sleep/mood work because the moderation lock is active, so long moderation runs cannot leave `lastSpokeAt` stale against delivered speech. + - Speaking-pressure freshness is resident physiology policy. Every five-minute due pass derives `lastSpokeAt` from canonical speech receipts and updates pressure directly; no mood script or moderation lock may override it. - `npm run moderation:agency-pressure:fixture` verifies typed agency pressure enters CultCache, renders in the self-state summary, and contributes to mood speaking pressure without reviving hard-wired agency behavior. - Memory cleanup now has to be meaningfully destructive. Sleep-side consolidation should translate old thought fossils into the live claim/question/fascination surface, drop templated slogan-slop, trim repo/archive/runtime residue down to a small recent anchor surface, and keep the freshest surviving records instead of accidentally embalming the oldest ones after a sort. - 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. @@ -289,4 +289,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 Windows-rumination deletion: no built-in Void participant, no moderation lock observation, no runner/installer, and no model activity while the resident rumination gate is false. Then cut the remaining manual PowerShell memory-maintenance and mood compatibility bodies now that physiology and nap-deduped memory maintenance are resident and verified. Keep the swarm paused and do not enable model cadences until a valid credential exists. + next_action: Deploy and verify the Windows rumination/mood/memory deletion: no built-in Void participant, no moderation lock observation, no deleted runner references, and no model activity while resident model gates are false. Confirm direct Void Discord mentions use the normal bot prompt lane and Persona attention contains only registered identities. Then continue extracting remaining Bifrost/watchdog/dashboard chores from the Local Orchestrator into their owning daemons.