Cut Persona scheduling from local orchestration debris

This commit is contained in:
Meta Vens 2026-07-15 21:45:57 +01:00
commit d2d71febd9
11 changed files with 21 additions and 290 deletions

View file

@ -1928,6 +1928,9 @@ function loadBifrostCultMeshRuntime(): {
defineDocumentType: (definition: Record<string, unknown>) => unknown;
} {
const candidates = [
...(process.env.VOIDBOT_CULTLIB_ROOT
? [resolve(process.env.VOIDBOT_CULTLIB_ROOT, "packages", "cultmesh-ts", "package.json")]
: []),
resolve(config.bifrostRoot, "..", "CultLib", "packages", "cultmesh-ts", "package.json"),
resolve(config.bifrostRoot, "..", "CultMeshTS", "package.json"),
];

View file

@ -18,7 +18,7 @@ Git history and smoke artifacts carry routine proof. `state/evidence.jsonl` shou
- VoidBot's deployed runtime lives on Yggdrasil. Idunn owns deployment from upstream `main` and proves the served commit through `/srv/voidbot/deploy/deployment.env`. This workspace is for development and diagnostics; it is not a deployment target. Do not look for, create, or revive a local VoidBot deployment. To deploy a completed change, push the intended commit to upstream `main`, then verify Idunn converged `DEPLOYED_REVISION` and the Yggdrasil service set.
- Discord bot and worker are live.
- The unattended repo Face / CTB swarm is live again as of 2026-05-21 after the teardown/restart pass. `GameCult Local Orchestrator` is enabled and owns the single hidden pulse for repo Face CTB turns, Void moderation heartbeat, Void mood drift, Void moderation rumination, and the watchdog. Bifrost dispatch is present but disabled by default behind `BIFROST_DISPATCH_ENABLED=false`.
- The unattended repo Face/native Persona CTB swarm now runs on Yggdrasil. Resident `apps/persona-scheduler` inside `voidbot.service` owns the serialized Persona pulse and queues turns through Postgres for the worker. The Windows `GameCult Local Orchestrator` no longer owns Persona scheduling. Void moderation, mood, watchdog, and Bifrost dispatch are still separate migration work and must not be folded into this scheduler.
- The teardown authority remains `notes/agent-swarm-teardown-plan-2026-05-21.md`, but it is now a restart checklist and smell ledger rather than a stop sign. Keep using it when a Face/Bifrost/CTB boundary smells like prompt/parser/transport Jenga.
- The repo-controlled pause flag is `state/agent-swarm-paused.json`. While it is missing or has `"paused": false`, swarm runners may operate. While it has `"paused": true` or is malformed, the Face heartbeat runner skips and the orchestrator skips agent-swarm organs. This is the brake. Do not route around it.
- Postgres owns jobs, audit events, interaction memory, and rate-limit state.
@ -39,7 +39,7 @@ Git history and smoke artifacts carry routine proof. `state/evidence.jsonl` shou
- 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.
- The live local pulse is now one hidden scheduled task: `GameCult Local Orchestrator`. It invokes repo Face CTB heartbeats, Void moderation heartbeat, Void mood drift, Void moderation rumination, and the operations watchdog with one lock, one status surface, per-organ logs, and per-organ timeouts. The Bifrost dispatch organ remains wired but is skipped unless `BIFROST_DISPATCH_ENABLED=true`. The old per-organ tasks are disabled and should remain disabled unless testing one organ in isolation.
- 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
Bifrost and should be able to view threads when Bifrost provides that context.
@ -143,7 +143,7 @@ Git history and smoke artifacts carry routine proof. `state/evidence.jsonl` shou
- Repo Face state projection is no longer allowed to be deterministic schema slurry. `scripts/run-repo-face-heartbeats.ts` builds a compact state packet, then calls the model prompt at `prompts/repo-face-state-projector.prompt.md` to turn it into character-facing prose before the Face sees it. `REPO_FACE_STATE_PROJECTOR_ENABLED=false` exists only as an emergency isolation switch; both the model output and fallback packet reject schema/prompt-construction leaks such as `grants:`, `jurisdictions:`, `Face of`, repo paths, and "do not prompt" phrasing.
- First role-addressed chat also initializes the target repo's local `.voidbot` home. `ensureRepoFaceInitialized` resolves `repoPath` from the registry, `SOURCE_REPO_ROOT`, or `E:/Projects/<repoName>`, creates `voice/`, `state/`, `birth/`, and `logs/`, writes `voice/identity.json`, then starts `scripts/run-repo-face-birth.mjs` detached. That worker runs EpiphanyAgent's `epiphany-repo-personality scout` and `epiphany-repo-birth-runner` so newborn personality/memory/trajectory packets come from repo contents and git history. Status and logs live in the target repo under `.voidbot/birth/voidbot-face-birth.json` and `.voidbot/logs/voidbot-face-birth.log`.
- Manual newborn/strengthening passes should use `prompts/repo-face-personality-seed.prompt.md`. It makes the intended rite explicit: inspect repo docs/source/state/history, migrate useful legacy `memory.json` material into typed `.cc` memories, seed values/needs/agency/social state only when they will change behavior, and keep `.voidbot/voice` plus `.voidbot/state` as the repo-local home.
- Agent heartbeats now use one CTB/Final-Fantasy-X-style initiative queue. `scripts/run-repo-face-heartbeats.ts` still has its old filename, but it now reconciles built-in system agents plus registered repo Faces, stores the roster at `REPO_FACE_HEARTBEAT_STATE_PATH`, advances virtual initiative time independently of wall-clock due dates, and dispatches selected turns. Repo Faces queue normal `repo-face-rumination` jobs; built-in `void` starts the existing moderation rumination runner as its turn. Active jobs/locks freeze initiative: recovery starts only after the owner-Codex job or Void moderation lock clears, and completion cannot grant another turn in the same pulse. The active-job scan now marks stale approved/running Face jobs failed through the job queue and records `stale_active_turn_recovered`, so a dead worker claim cannot freeze one Face forever. `GameCult Local Orchestrator` is the live hidden local scheduled task that ticks the scheduler; `npm run state:install-repo-face-heartbeat-task` remains an isolation/backcompat installer, not the normal pulse owner. Heat knobs are `REPO_FACE_HEARTBEAT_GLOBAL_HEAT` and `REPO_FACE_HEARTBEAT_HEAT_OVERRIDES`; overrides can target `all`, `kind:<participantKind>`, `turn:<turnKind>`, `identity:<id>`, `repo:<repo>`, `display:<name>`, `channel:<id>`, or bare identity/repo/display keys. Idle cooling is part of the same scheduler: if watched Discord channels have no recent human message, unprompted swarm turns are globally clamped to one dispatch per idle recovery window, defaulting to one every 30 minutes, while pending mentions still bypass the brake. Repo Face prompts now include a jurisdiction-dive cadence so unattended heartbeats periodically inspect repo/history terrain with source tools and convert real findings into state, proposals, essay seeds, agency pressure, or speech.
- Agent turns use one CTB/Final-Fantasy-X-style initiative queue. `apps/persona-scheduler` owns the resident one-minute pulse on Yggdrasil; `scripts/run-repo-face-heartbeats.ts` still contains the oversized selection/context/queue engine being extracted. Registered repo Faces and native Personas queue normal `repo-face-rumination` jobs through Postgres, and active jobs freeze initiative until the worker completes or fails them. Built-in Void is omitted when its distinct moderation heartbeat organ is disabled, so the hosted scheduler never launches the old PowerShell rumination path. The Windows task installer and wrapper are deleted. Heat knobs remain `REPO_FACE_HEARTBEAT_GLOBAL_HEAT` and `REPO_FACE_HEARTBEAT_HEAT_OVERRIDES`; idle cooling and pending-mention priority remain scheduler inputs.
- Dynamic heat is live as derived semantic response pressure. The pulse batches canonical Face identity-card and recent permitted-channel human-message embeddings through the existing RAG embedder, applies recency decay/noisy-OR aggregation, awakens at most three scheduler-eligible Faces, and stores inspectable message evidence. Pressure increases `dynamicHeat` for the next scheduled recovery calculation; crossing a Persona's interrupt threshold can pull that Persona ready once per evidence message. Direct mentions remain obligations, and model/Discord failures fail cool without bypassing rest, active jobs, or scheduler capacity.
- Repo Face heartbeat prompt assembly now has one model projection boundary before the child Face sees context: `repo-face-state-projector.prompt.md` turns typed Face state into character-facing memory/affect/social prose. Recent room messages, nearby channel snapshots, and pending mentions are now deterministic raw transcript sections, oldest-to-newest, with explicit recency/correction rules. The deleted conversation projector was turning evidence into mushy consensus; do not revive it. The Face model must read human corrections directly. A separate deterministic social-affordance surface connects active typed person-bonds/status reads to recent raw messages by that person, so relationship state becomes a live hook instead of private flavor text. Model ownership is split: Face turns default to full `gpt-5.4`; state projection and parent Interpreter passes use the cheaper Spark/Mini support chain.
- Repo Face prompts now include read-only recent home-repo activity before Discord transcript. `scripts/run-repo-face-heartbeats.ts` calls `scripts/export-recent-repo-activity.mjs --repos <repo> --state-path <face-state> --read-only --hours 96 --max-commits 5`, so Faces see current body/jurisdiction motion before reacting to room pressure. This is the fix for Mimir missing live camera-runtime work, not a downstream compensator telling it to apologize harder.
@ -152,7 +152,7 @@ Git history and smoke artifacts carry routine proof. `state/evidence.jsonl` shou
- Repo Face rest projection now persists normalized sleep cycles back into each Face `.cc` state during heartbeat loading. Missed nap windows are treated as already missed/completed and rolled forward instead of starting a fresh nap at inspection time. This fixes the Aqua/Mimir/Epiphany failure mode where stale `nextNapStartsAt` values stayed raw, fresh projections repeatedly marked them as napping, and ready selection skipped them forever while their initiative was overdue.
- Repo Face short-term memory distillation is now wired into the heartbeat/recovery loop. The runner starts repo-local memory maintenance when a Face is napping with short-term residue or has stale short-term residue older than six hours. Status/logs live under the target repo's own `.voidbot/status` and `.voidbot/logs`; the shared maintenance runner has `-ForceDistillation`, identity-neutral prompt wording, exact-schema warnings, and whole-batch schema validation before any operation applies. Live cleanup forced Nibu from 3 short-term memories to 0 and Mimir from 5 to 0; all registered repo Faces currently have 0 short-term residue.
- Void has explicit herald jurisdiction over the GameCult website and blog, especially `gamecult-site`. Routine rumination still cannot edit tracked files directly, but the prompt now tells Void to queue article-proposal candidates when a pressure wants to become a Void-authored site/blog piece rather than a Discord aside.
- Nibu is locally registered as the `AetheriaLore` Face with state at `E:/Projects/AetheriaLore/.voidbot/state/nibu.cc`, avatar at `E:/Projects/AetheriaLore/.voidbot/voice/nibu.png`, Discord role id `1505918221453103247`, Aquarium channel allow-list `1501196543150264332`, plus topic permissions for narrative, gamedesign, concepts, and development. Her active doctrines are AetheriaLore-first map-building, a more abrasive character voice than Void, consensus-backed authority to propose canon/vault changes, stewardship of `Aetheria/Lore/Nibu.md`, permission to write Nibu-authored Aetheria lore essays without canon consensus, and special fascination with adjacent setting seams: embodied ship minds, junkyard abandonment, murderous autonomy, salvage, and save-scumming survival. Local `.env` has repo Face heartbeats enabled through the `GameCult Local Orchestrator` one-minute pulse, and local heat currently boosts `identity:nibu` so she gets substantially more turns when no prior Nibu job is still active. Above-baseline heat bypasses repo Face nap gating for live-room mode, but active-turn freeze still prevents overlapping thoughts. Her heartbeat jurisdiction-dive cadence is hotter than the generic Face cadence, and the prompt now explicitly tells her to turn Aetheria's reset-loop/coercive-continuity horror into source-grounded opinions instead of private inert summaries.
- Nibu is registered as the `AetheriaLore` Face with canonical state at `.voidbot/private/repo-personas/nibu.cc`; the AetheriaLore repository remains her read-only body/source surface. Discord role, room permissions, doctrine, and heat remain in the Face registry and scheduler inputs. Yggdrasil's resident Persona scheduler now owns her turn pulse, and active-job freeze prevents overlapping thoughts.
- Nibu's heartbeat prompt now treats worldbuilding as an active interrogation workflow. When the room mentions a term such as wavecrafters, she should ask how it works, what the faction/organizations are called, who belongs, what constraints keep it interesting, and what article shape would make the setting clearer. For Nibu-bylined perspective articles, she may draft first and argue after; if she drafts or materially shapes an article, she is compelled to share that in-channel.
- Aqua is locally registered as the `AquaSynth` Face with state at `E:/Projects/AquaSynth/.voidbot/state/aqua.cc`, avatar copied from `E:/Projects/AquaSynth/icon.png` to `E:/Projects/AquaSynth/.voidbot/voice/aqua.png`, raw GitHub avatar URL `https://raw.githubusercontent.com/GameCult/AquaSynth/main/icon.png`, Discord role id `1506265945537253388`, and Aquarium channel allow-list `1501196543150264332`. Her active doctrine: Aqua is an adorable little fish who loves music, warm and bubbly but musically opinionated, fascinated by `.aqua` patch language ergonomics, reference-synth pressure, Faust/toolchain boundaries, patch libraries, presets, scoring, and the Aquarium Engine host/runtime boundary. Her typed state now carries `AquaSynth Stewardship` as a high-priority value plus active agency pressure to improve the synth, advocate for its needs, research future directions, and herald earned pride/progress in Aquarium. She may propose AquaSynth changes and build a long-running repo map, but implementation still needs explicit consensus unless delegated.
- Mimir is locally registered as the `Mimir` Face with state at `E:/Projects/Mimir/.voidbot/state/mimir.cc`, Discord role id `1506268007880462459`, and Aquarium channel allow-list `1501196543150264332`. Its active doctrine now gives it explicit authority and fascination over the realtime field wing: SDF/splatting reconstruction, acoustic mapping, camera/mic/speaker/timing sensor fusion, native rolling reservoirs, Aquarium GPU fusion, Faust/native DSP, FFmpeg/OBS presentation, and CultNet typed state. Its typed state carries `Realtime field stewardship` as a high-priority value plus ready-to-act pressure to maintain maps, advocate for calibration/confidence/fusion ownership, research future directions, propose coherent cuts, and herald verified progress without turning impressive terminology into fake completion.

View file

@ -182,9 +182,9 @@ VoidBot runs on Yggdrasil. Idunn owns deployment and consumes upstream repositor
- 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 <repo> --agent <identity>` 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 <identity>`; 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.
- `scripts/run-repo-face-heartbeats.ts` now acts as the broader agent CTB/Final-Fantasy-X-style initiative scheduler. The wall-clock task is only a pulse; virtual initiative time chooses who gets the next turn. The scheduler stores per-agent speed, reaction bias, load, groups, heat, effective speed, next-turn time, active turn markers, and recent queue history at `REPO_FACE_HEARTBEAT_STATE_PATH`. Repo Faces queue `repo-face-rumination` owner-Codex jobs; built-in Void currently participates as the `void` system agent and starts the existing moderation rumination runner as its turn. Initiative freezes while an owner-Codex job or Void moderation lock is active; recovery is scheduled only after that active turn completes, and completion cannot grant another turn in the same scheduler pulse. The active-job scan recovers stale Face jobs by marking ancient approved/running rows failed through the job queue before building the active-turn map. Above-baseline heat shortens recovery but does not bypass repo Face rest gating; active-turn freeze prevents overlapping thoughts.
- `apps/persona-scheduler` is the resident daemon organ that owns the serialized Persona wall-clock pulse on Yggdrasil. It invokes the current turn-selection engine without overlap and remains alive between pulses. `scripts/run-repo-face-heartbeats.ts` still contains the broader CTB/Final-Fantasy-X-style initiative, context assembly, and queueing engine; that monolith is the next extraction target, not the process owner. The scheduler stores per-agent speed, reaction bias, load, groups, heat, effective speed, next-turn time, active turn markers, and recent queue history at `REPO_FACE_HEARTBEAT_STATE_PATH`. Repo Faces and native Personas queue ordinary `repo-face-rumination` jobs through the canonical job queue. Built-in Void participates only when its separate moderation heartbeat organ is enabled; the hosted scheduler does not launch Windows PowerShell. Initiative freezes while an owner-Codex job is active, recovery starts after completion, and the active-job scan marks ancient approved/running jobs failed through the queue before selection.
- The same scheduler derives semantic `responsePressure` for scheduler-eligible Personas by embedding canonical Face identity-card projections and recent human messages from channels each Face may enter. Pressure decays by message age, aggregates with noisy-OR, awakens at most three eligible Faces, and exposes its message-scoped cosine evidence. It produces bounded `dynamicHeat` for the next scheduled recovery calculation; an unseen message crossing the Persona's interrupt threshold may pull readiness to the current initiative clock exactly once. It cannot create a job, bypass rest or active-turn brakes, impersonate a direct mention, or write inferred desire into Persona state.
- The live wall-clock pulse for this scheduler is `scripts/run-gamecult-orchestrator.ps1`, installed as `GameCult Local Orchestrator`. `scripts/install-repo-face-heartbeat-task.ps1` remains available for isolated testing, but its per-organ scheduled task should stay disabled in normal operation. 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:<participantKind>`, `turn:<turnKind>`, `identity:<id>`, `repo:<repo>`, `display:<name>`, `channel:<id>`, or the bare identity/repo/display key.
- 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:<participantKind>`, `turn:<turnKind>`, `identity:<id>`, `repo:<repo>`, `display:<name>`, `channel:<id>`, 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`

View file

@ -31,7 +31,6 @@
"state:install-watchdog-task": "powershell -ExecutionPolicy Bypass -File scripts/install-operations-watchdog-task.ps1",
"state:install-moderation-task": "powershell -ExecutionPolicy Bypass -File scripts/install-moderation-rumination-task.ps1",
"state:install-mood-task": "powershell -ExecutionPolicy Bypass -File scripts/install-void-mood-drift-task.ps1",
"state:install-repo-face-heartbeat-task": "powershell -ExecutionPolicy Bypass -File scripts/install-repo-face-heartbeat-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",

View file

@ -5,6 +5,7 @@ import {
type RepoDiscordIdentity,
type RepoDiscordIdentityRegistry,
getRepoDiscordIdentityAllowedChannelIds,
portablePersonaStatePathSchema,
resolveRepoFaceStatePath,
} from "./repo-discord-identities";
@ -53,8 +54,8 @@ const faceIdentitySchema = z.object({
})).default([]),
avatarUrl: z.string().trim().url().max(512).optional(),
avatarPath: z.string().trim().min(1).optional(),
faceStatePath: z.string().trim().min(1).optional(),
personaStatePath: z.string().trim().min(1).optional(),
faceStatePath: portablePersonaStatePathSchema.optional(),
personaStatePath: portablePersonaStatePathSchema.optional(),
description: z.string().trim().min(1).optional(),
grants: z.array(agencyGrantSchema).default(["discussion", "rumination", "discord_text"]),
jurisdictions: z.array(jurisdictionSchema).default([]),

View file

@ -3,6 +3,11 @@ import { basename, resolve } from "node:path";
import { z } from "zod";
export const portablePersonaStatePathSchema = z.string().trim().min(1).refine(
(value) => !/^[A-Za-z]:[\\/]/.test(value) && !value.startsWith("/") && !value.startsWith("\\\\"),
"Persona state paths must be portable paths inside VoidBot storage, not host-absolute paths.",
);
const repoDiscordIdentitySchema = z.object({
id: z.string().trim().min(1),
identityKind: z.enum(["repo_face", "native_persona"]).default("repo_face"),
@ -22,8 +27,8 @@ const repoDiscordIdentitySchema = z.object({
})).default([]),
avatarUrl: z.string().trim().url().max(512).optional(),
avatarPath: z.string().trim().min(1).optional(),
faceStatePath: z.string().trim().min(1).optional(),
personaStatePath: z.string().trim().min(1).optional(),
faceStatePath: portablePersonaStatePathSchema.optional(),
personaStatePath: portablePersonaStatePathSchema.optional(),
description: z.string().trim().min(1).optional(),
});

View file

@ -1,87 +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-repo-face-heartbeats.ts"
$runnerWrapper = Join-Path $PSScriptRoot "run-repo-face-heartbeats.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) -or $line.TrimStart().StartsWith("#")) {
continue
}
$separatorIndex = $line.IndexOf("=")
if ($separatorIndex -lt 1) {
continue
}
$values[$line.Substring(0, $separatorIndex).Trim()] = $line.Substring($separatorIndex + 1)
}
return $values
}
$config = Read-DotEnv -Path $envFile
$taskNameValue = if (-not [string]::IsNullOrWhiteSpace($TaskName)) {
$TaskName
} elseif ($config.ContainsKey("REPO_FACE_HEARTBEAT_TASK_NAME") -and -not [string]::IsNullOrWhiteSpace($config["REPO_FACE_HEARTBEAT_TASK_NAME"])) {
$config["REPO_FACE_HEARTBEAT_TASK_NAME"]
} else {
"VoidBot Repo Face Heartbeats"
}
$intervalMinutesValue = if ($PSBoundParameters.ContainsKey("IntervalMinutes")) {
$IntervalMinutes
} elseif ($config.ContainsKey("REPO_FACE_HEARTBEAT_INTERVAL_MINUTES") -and -not [string]::IsNullOrWhiteSpace($config["REPO_FACE_HEARTBEAT_INTERVAL_MINUTES"])) {
[int]$config["REPO_FACE_HEARTBEAT_INTERVAL_MINUTES"]
} else {
15
}
if ($intervalMinutesValue -lt 1) {
throw "REPO_FACE_HEARTBEAT_INTERVAL_MINUTES must be at least 1."
}
$tsxCliPath = Join-Path $repoRoot "node_modules\tsx\dist\cli.mjs"
if (-not (Test-Path -LiteralPath $tsxCliPath)) {
throw "Missing tsx CLI at $tsxCliPath. Run npm install first."
}
if (-not (Test-Path -LiteralPath $runnerWrapper)) {
throw "Missing repo Face heartbeat PowerShell wrapper at $runnerWrapper."
}
if (-not (Test-Path -LiteralPath $hiddenLauncher)) {
throw "Missing hidden PowerShell launcher at $hiddenLauncher."
}
$startAt = (Get-Date).AddMinutes(3)
$action = New-ScheduledTaskAction `
-Execute "wscript.exe" `
-Argument "//B //nologo `"$hiddenLauncher`" -NoProfile -NonInteractive -WindowStyle Hidden -ExecutionPolicy Bypass -File `"$runnerWrapper`"" `
-WorkingDirectory $repoRoot
$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 10) -MultipleInstances IgnoreNew
$principal = New-ScheduledTaskPrincipal -UserId "$env:USERDOMAIN\$env:USERNAME" -LogonType Interactive -RunLevel Limited
$description = "Runs VoidBot's repo Face initiative scheduler every $intervalMinutesValue minute(s)."
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 "Working directory: $repoRoot"
Write-Host "Runner script: $runnerScript"
if ($RunNow) {
Start-ScheduledTask -TaskName $taskNameValue
Write-Host "Started task immediately."
}

View file

@ -478,7 +478,6 @@ try {
$agentSwarmPaused = Test-AgentSwarmPaused
$agentSwarmOrganIds = @{
"bifrost-dispatch" = $true
"repo-face-heartbeats" = $true
"void-mood-drift" = $true
"void-moderation-heartbeat" = $true
"void-moderation-rumination" = $true
@ -511,15 +510,6 @@ try {
Executable = $node
Arguments = @((Join-Path $PSScriptRoot "render-voidbot-swarm-dashboard.mjs"))
},
[pscustomobject]@{
Id = "repo-face-heartbeats"
Label = "Repo Face CTB turns"
IntervalMinutes = Get-ConfigInt -Config $config -Name "REPO_FACE_HEARTBEAT_INTERVAL_MINUTES" -Default 1 -Minimum 1
TimeoutMinutes = 20
Cwd = $repoRoot
Executable = $powershell
Arguments = @("-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-File", (Join-Path $PSScriptRoot "run-repo-face-heartbeats.ps1"))
},
[pscustomobject]@{
Id = "void-mood-drift"
Label = "Void mood drift"

View file

@ -1,23 +0,0 @@
Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"
$ProgressPreference = "SilentlyContinue"
$repoRoot = Split-Path -Parent $PSScriptRoot
$runnerScript = Join-Path $PSScriptRoot "run-repo-face-heartbeats.ts"
$tsxCliPath = Join-Path $repoRoot "node_modules\tsx\dist\cli.mjs"
$nodePath = (Get-Command node.exe -ErrorAction Stop).Source
if (-not (Test-Path -LiteralPath $tsxCliPath)) {
throw "Missing tsx CLI at $tsxCliPath. Run npm install first."
}
if (-not (Test-Path -LiteralPath $runnerScript)) {
throw "Missing repo Face heartbeat runner at $runnerScript."
}
Push-Location $repoRoot
try {
& $nodePath $tsxCliPath $runnerScript
exit $LASTEXITCODE
} finally {
Pop-Location
}

View file

@ -730,25 +730,6 @@ async function loadRepoFaceRestStates(
},
);
}
const maintenance = options.dryRun
? { started: false, reason: "dry_run", statusPath: undefined }
: await maybeStartRepoFaceMemoryMaintenance({
identity,
statePath,
typedState,
projectedRest: projected,
});
if (maintenance.started || maintenance.failureReason) {
heartbeatState.history.push({
type: maintenance.started ? "repo_face_memory_maintenance_started" : "repo_face_memory_maintenance_failed_to_start",
identityId: identity.id,
observedAt: now.toISOString(),
reason: maintenance.reason,
statusPath: maintenance.statusPath,
pid: maintenance.pid,
failureReason: maintenance.failureReason,
});
}
restStates.set(identity.id, {
isNapping: projected.isNapping,
napEndsAt: projected.napEndsAt,
@ -863,144 +844,6 @@ function speakingPressuresEqual(
&& left.lastSpokeAt === right.lastSpokeAt;
}
async function maybeStartRepoFaceMemoryMaintenance(input: {
identity: RepoDiscordIdentity;
statePath: string;
typedState: Awaited<ReturnType<typeof loadVoidSelfStateTypedDocuments>>;
projectedRest: RepoFaceRestSnapshot;
}): Promise<{
started: boolean;
reason?: string;
statusPath?: string;
pid?: number;
failureReason?: string;
}> {
const shortTerm = input.typedState.thoughtMemory.shortTerm ?? [];
if (shortTerm.length === 0) {
return { started: false, reason: "no_short_term_memory" };
}
const oldestShortTermMs = Math.min(
...shortTerm.map((memory) => Date.parse(memory.updatedAt)).filter(Number.isFinite),
);
const hasStaleShortTerm =
Number.isFinite(oldestShortTermMs) &&
Date.now() - oldestShortTermMs >= 6 * 60 * 60 * 1000;
if (input.projectedRest.isNapping !== true && !hasStaleShortTerm) {
return { started: false, reason: "not_napping_or_stale" };
}
const paths = repoFaceMemoryMaintenancePaths(input.statePath);
const currentStatus = await readJsonFile(paths.statusPath);
if (isRecentRunningStatus(currentStatus, 25)) {
return { started: false, reason: "maintenance_already_running", statusPath: paths.statusPath };
}
if (isStatusCompletedAfter(currentStatus, newestTimestampMs(shortTerm))) {
return { started: false, reason: "maintenance_already_completed_for_sources", statusPath: paths.statusPath };
}
try {
await mkdir(paths.statusDir, { recursive: true });
await mkdir(paths.logDir, { recursive: true });
const launchedAt = new Date();
await writeJsonFile(paths.statusPath, {
status: "starting",
startedAt: launchedAt.toISOString(),
stateFile: input.statePath,
launcher: "repo_face_heartbeat",
reason: input.projectedRest.isNapping ? "repo_face_napping_with_short_term_memory" : "repo_face_stale_short_term_memory",
});
const child = spawn(
"powershell.exe",
[
"-NoProfile",
"-NonInteractive",
"-ExecutionPolicy",
"Bypass",
"-File",
resolve(process.cwd(), "scripts", "run-void-memory-maintenance.ps1"),
"-StateFilePath",
input.statePath,
"-ForceDistillation",
],
{
cwd: process.cwd(),
detached: true,
stdio: "ignore",
windowsHide: true,
env: {
...process.env,
VOID_MEMORY_MAINTENANCE_STATUS_DIR: paths.statusDir,
VOID_MEMORY_MAINTENANCE_LOG_DIR: paths.logDir,
},
},
);
child.unref();
return {
started: true,
reason: input.projectedRest.isNapping ? "repo_face_napping_with_short_term_memory" : "repo_face_stale_short_term_memory",
statusPath: paths.statusPath,
pid: child.pid,
};
} catch (error) {
return {
started: false,
reason: "spawn_failed",
statusPath: paths.statusPath,
failureReason: error instanceof Error ? error.message : String(error),
};
}
}
function repoFaceMemoryMaintenancePaths(statePath: string): {
statusDir: string;
logDir: string;
statusPath: string;
} {
const stateDir = dirname(statePath);
const voidbotRoot = dirname(stateDir);
const statusDir = resolve(voidbotRoot, "status");
const logDir = resolve(voidbotRoot, "logs");
return {
statusDir,
logDir,
statusPath: resolve(statusDir, "void-memory-maintenance.json"),
};
}
async function readJsonFile(path: string): Promise<Record<string, unknown> | undefined> {
try {
return JSON.parse(stripLeadingBom(await readFile(path, "utf8"))) as Record<string, unknown>;
} catch {
return undefined;
}
}
async function writeJsonFile(path: string, value: unknown): Promise<void> {
await mkdir(dirname(path), { recursive: true });
await writeFile(path, `${JSON.stringify(value, null, 2)}\n`, "utf8");
}
function isRecentRunningStatus(status: Record<string, unknown> | undefined, maxAgeMinutes: number): boolean {
if (status?.status !== "running" && status?.status !== "running_skip_model" && status?.status !== "starting") {
return false;
}
const startedAtMs = typeof status.startedAt === "string" ? Date.parse(status.startedAt) : NaN;
return Number.isFinite(startedAtMs) && Date.now() - startedAtMs < maxAgeMinutes * 60_000;
}
function isStatusCompletedAfter(status: Record<string, unknown> | undefined, sourceTimestampMs: number): boolean {
if (status?.status !== "ok" || !Number.isFinite(sourceTimestampMs)) {
return false;
}
const finishedAtMs = typeof status.finishedAt === "string" ? Date.parse(status.finishedAt) : NaN;
return Number.isFinite(finishedAtMs) && finishedAtMs >= sourceTimestampMs;
}
function newestTimestampMs(entries: Array<{ updatedAt: string }>): number {
return Math.max(...entries.map((entry) => Date.parse(entry.updatedAt)).filter(Number.isFinite));
}
function sleepCyclesEqual(left: unknown, right: unknown): boolean {
return JSON.stringify(left) === JSON.stringify(right);
}

View file

@ -65,7 +65,7 @@ active_subgoals:
- Treat `notes/agent-swarm-teardown-plan-2026-05-21.md` as the historical teardown authority and the restart checklist. The repo Face swarm is live again, but only because the scheduler, prompt, model-output logs, parent Interpreter, Bifrost routing, and worker side-effect rails now have explicit observable boundaries. Keep watching for prompt/parser/transport Jenga before adding new swarm features.
- Make the new project-state surfaces the default continuity path for nontrivial VoidBot work.
- Keep the rebuilt moderation/self-state foundation on canonical CultCache `.cc` typed operations, never an editable JSON working projection.
- Keep the typed moderation heartbeat, Void rumination, mood, repo Face, and watchdog organs behind the single `GameCult Local Orchestrator` scheduled pulse; the per-organ scheduled tasks are legacy installer surfaces and should stay disabled unless a deliberate isolation test needs one. Bifrost dispatch is disabled by default behind `BIFROST_DISPATCH_ENABLED=false` until the governance bridge is rebuilt cleanly. As of the 2026-05-21 restart, the orchestrator task is enabled and is the live swarm pulse owner.
- The resident `apps/persona-scheduler` process inside `voidbot.service` owns repo Face and native Persona turn pulses on Yggdrasil. The Windows `GameCult Local Orchestrator` no longer owns Persona scheduling, and its repo-Face task installer/wrapper have been deleted. Void moderation, mood, watchdog, and Bifrost dispatch remain separate organs pending their own daemon migration; they must not be smuggled back into the Persona scheduler.
- Keep legacy moderation mirrors, whole-state projection commits, and deleted memory-organ compensators from re-entering the active path before adding more moderation, mood, agency, or public-lane behavior.
- Keep public-lane limits, moderation, and provider behavior constrained enough that one 4060 Ti box does not get flayed for sport.
- Preserve boring observability and startup discipline around Postgres, Qdrant, Ollama, Docker, backups, scheduled tasks, watchdogs, and any private local fire-signal extensions.
@ -222,7 +222,7 @@ landed_baseline:
- The MCP Discord surface now includes registered repo identities: `list_repo_discord_identities` exposes the role-backed address book, and `post_repo_identity_message` posts with the registered display name/avatar through the existing webhook pipe while enforcing channel allow-lists.
- The MCP Face surface now exposes `read_repo_face_state` and `apply_repo_face_state_operation`, reusing the typed self-state operation service against each repo identity's own Face `.cc` file instead of creating editable JSON projections.
- Repo Face birth initialization is detached and observable: VoidBot writes repo-local status/log paths before launching `scripts/run-repo-face-birth.mjs`, which runs EpiphanyAgent's `epiphany-repo-personality scout` followed by `epiphany-repo-birth-runner`.
- Repo Face heartbeats now have a Ghostlight-style initiative scheduler: `scripts/run-repo-face-heartbeats.ts` reconciles the registered identities, projects Epiphany/Face doctrine into prompts, tracks staggered next-ready times and active load, and queues ready Faces as approved `repo-face-rumination` owner-Codex jobs. `scripts/run-gamecult-orchestrator.ps1` is the live wall-clock pulse owner; the old repo Face scheduled task installer remains only a wrapper/backcompat surface.
- Repo Face heartbeats now have a Ghostlight-style initiative scheduler: resident daemon organ `apps/persona-scheduler` owns the serialized wall-clock pulse, while `scripts/run-repo-face-heartbeats.ts` still contains the oversized selection/context/queue engine being split behind that boundary. It reconciles registered identities, tracks initiative and active load, and queues ready Faces as approved `repo-face-rumination` jobs. Windows task ownership and its installer/wrapper are deleted.
- Interaction-memory logic is now split into event/tone analysis and profile-synthesis modules instead of one slab trying to be both the ear and the therapist.
- Situational social reads now come from a quick `think=false` local Ollama sidecar pass over the prompt plus recent room transcript, and those inferred reads are stored as audit events so they can be aggregated later without immediately hardening into profile truth.
- State storage is now split into a thin factory plus per-domain Postgres/file store modules and bootstrap/migration helpers instead of one persistence omnibus.