From 8441bdec322e14e08284c75c19410ae886e8e1b7 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Tue, 26 May 2026 15:06:56 +0100 Subject: [PATCH] Reinitialize CTB spacing on swarm wake --- notes/fresh-workspace-handoff.md | 3 +- notes/voidbot-current-system-map.md | 2 + scripts/run-repo-face-heartbeats.ts | 130 ++++++++++++++++++++++++++++ state/map.yaml | 5 +- 4 files changed, 137 insertions(+), 3 deletions(-) diff --git a/notes/fresh-workspace-handoff.md b/notes/fresh-workspace-handoff.md index f179ad1..4185b9f 100644 --- a/notes/fresh-workspace-handoff.md +++ b/notes/fresh-workspace-handoff.md @@ -138,6 +138,7 @@ Git history and smoke artifacts carry routine proof. `state/evidence.jsonl` shou - Worker delivery must treat Interpreter channel names as Face-local labels, not raw Discord ids. `SAY channel: aquarium` or `channel: narrative` resolves through the registered identity's `channelPermissions` before allow-list checks and webhook delivery. Earlier live Aqua/Nibu turns parsed `say=1` but did not post because the worker literally checked whether the identity was allowed in channel id `aquarium`; the post path now resolves labels and has a live Epiphany webhook receipt after restart. - 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 owned by a separate maintenance-only path, not by public CTB speech scheduling. `scripts/run-gamecult-orchestrator.ps1` runs `repo-face-memory-maintenance` every minute through `scripts/run-repo-face-heartbeats.ps1 --maintenance-only --max-maintenance-runs `, while `repo-face-heartbeats` remains skippable under the swarm pause brake. The maintenance path retries stale launcher `starting` records, runs `scripts/run-void-memory-maintenance.ps1` synchronously with stdout/stderr captured under the target repo's `.voidbot/logs`, and records completion/failure in heartbeat history. The shared maintenance runner now uses a bounded workspace-write sandbox rooted at the status directory and the prompt explicitly forbids repo inspection, tests, helpers, tracked-file edits, or background work. Live cleanup on 2026-05-26 consumed stale short-term residue across all registered repo Faces; current registered Face short-term counts are 0. +- Repo Face CTB wake spacing is now explicit. When the heartbeat resumes after swarm pause, after collective nap-mode maintenance, after cold start, or after a non-hot all-ready collapse, `scripts/run-repo-face-heartbeats.ts` re-spaces unprotected active Faces across the base recovery window before selecting jobs. Pending mentions and manual turn pulls keep priority; intentionally high heat can still produce a real backlog if Faces become ready faster than `maxJobsPerTick` can drain them. A dashboard where every Face is `ready` should now mean overload, not ordinary wakeup. - 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'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. @@ -170,7 +171,7 @@ Git history and smoke artifacts carry routine proof. `state/evidence.jsonl` shou - Identity collapse has started. Active bot, worker, MCP repo-identity tools, and Face CTB scheduler paths now load through the canonical Face registry and treat repo Discord identities as compatibility projections. Next bounded move is to finish removing remaining direct conceptual dependence on repo identities, then split the Face turn loop into scheduler, context assembler, prompt renderer, candidate parser, policy gate, and transport adapter. - Repo Face child prompts now load the full global Codex `AGENTS.md` as inherited doctrine near the top of each Face turn. This deliberately carries the Prime Directive, rebuild contract, humor doctrine, infrastructure habits, Epiphany stewardship, and Great Colossus mission frame into Face cognition rather than relying on a local summary shard. - CotSC Praxis now lives in the global Codex `AGENTS.md` prompt surface rather than a repo-local prompt fragment. The reference note remains `notes/cotsc-praxis-worldview-2026-05-26.md`. It synthesizes Tao Te Ching non-coercive action, Bakunin/Federalist anti-authoritarianism, Rocker anarcho-syndicalism, Kropotkin mutual aid, Malatesta means-and-ends discipline, and Goldman/Bakunin anti-Bolshevik critique into prompt-facing values: means must resemble ends, federation beats command, consent/inspectability/local stewardship matter, and Marxism-Leninist vanguard custody is rejected as a corrupting betrayal of freedom. Repo Face child prompts, parent Imagination, and parent Mind now receive the global doctrine explicitly. -- CotSC Praxis also has an offline Socratic Ink article harness. `scripts/weave-face-socratic-ink.mjs` takes a scenario file, assembles real Face prompts, runs read-only Face child turns plus parent Mind normalization, simulates local CTB initiative, offers the next three Faces as choices, spends only the selected Face on that branch, and lets Void fold the discussion after one to three Face turns. The first scenario is `scenarios/cotsc-praxis-socratic-sermon.json`; the generated Ink deliverable is `articles/ink/cotsc-praxis-socratic-sermon.ink`; receipts live under `.voidbot/artifacts/socratic-ink/`. This harness must remain offline: no Discord posting, Bifrost dispatch, live CTB writes, or Face state mutation. +- CotSC Praxis also has an offline Socratic Ink article harness. `scripts/weave-face-socratic-ink.mjs` takes a scenario file, assembles real Face prompts, runs read-only Face child turns plus parent Mind normalization, simulates local CTB initiative, offers the next three Faces as choices, spends only the selected Face on that branch, and lets Void fold the discussion after one to three Face turns. Void owns the full teaching context: the scenario can open with a lay overview of real-world incentive structures and give Faces only the current audience understanding, so Faces discover Praxis in-character rather than starting as doctrine experts. The first scenario is `scenarios/cotsc-praxis-socratic-sermon.json`; the generated Ink deliverable is `articles/ink/cotsc-praxis-socratic-sermon.ink`; receipts live under `.voidbot/artifacts/socratic-ink/`. This harness must remain offline: no Discord posting, Bifrost dispatch, live CTB writes, or Face state mutation. - Repo Face child turns now preserve Discord media context. `SourceMessage` carries attachment metadata, heartbeat Discord fetch caches image attachments under `.voidbot/media/discord-images`, Face transcripts show `[media: image ... local=...]`, and owner Codex receives cached images through `codex exec --image`. Non-image attachments are visible as metadata only; Faces are explicitly told not to pretend they saw images unless a local image path was attached. - After those gates exist, run dry/mock Face turns before re-enabling `GameCult Local Orchestrator`. - The direct-ask receipt check remains important, but it is no longer the next move while the swarm is offline. diff --git a/notes/voidbot-current-system-map.md b/notes/voidbot-current-system-map.md index f973723..8c5a37d 100644 --- a/notes/voidbot-current-system-map.md +++ b/notes/voidbot-current-system-map.md @@ -158,6 +158,8 @@ This note is the source-grounded description of how the live VoidBot stack is sh - `scripts/run-repo-face-heartbeats.ts --maintenance-only` - repo Face Life path. It loads the registered Face states, projects sleep/rest state, starts repo-local memory maintenance for napping or stale short-term residue, and exits without queuing public CTB turns. The orchestrator calls this through `scripts/run-repo-face-heartbeats.ps1 --maintenance-only --max-maintenance-runs `. - Maintenance starts are synchronous and observable: stale launcher-only `starting` records without process evidence stop blocking retries after a short grace period, stdout/stderr are captured under the target repo's `.voidbot/logs`, and success/failure is recorded in the shared heartbeat history. Detached launch is no longer treated as proof that distillation happened. +- `scripts/run-repo-face-heartbeats.ts` + - repo Face CTB scheduler. After participant reconciliation, pending mention priority, and manual pulls, it reinitializes wake spacing when the live CTB has collapsed: cold start, wake from `agent_swarm_paused`, wake from collective nap-mode maintenance, or non-hot all-ready pileups. Protected direct summons stay due immediately; ordinary unprotected Faces are spread across the base recovery window so waking the swarm does not leave every active participant marked `ready`. - `scripts/run-void-memory-maintenance.ps1` - repo Face and Void typed memory-maintenance runner. It now runs the Codex child from the status directory with `workspace-write` sandbox by default, because the child only needs to read the provided context and write the operation output file. The prompt explicitly forbids repo inspection, tests, helper launches, tracked-file edits, and background work. - Repo Discord identities diff --git a/scripts/run-repo-face-heartbeats.ts b/scripts/run-repo-face-heartbeats.ts index 3c4bbdb..75961b7 100644 --- a/scripts/run-repo-face-heartbeats.ts +++ b/scripts/run-repo-face-heartbeats.ts @@ -376,6 +376,13 @@ async function main(): Promise { rescheduleStaleOverdueParticipants(state); applyPendingMentionPriority(state); const manualOverrideIdentities = applyManualTurnRequests(state, now); + reinitializeCtbSpacingOnWake({ + state, + restStates: runtimeSnapshots.restStates, + idleCooling, + maxJobs: config.repoFaceHeartbeats.maxJobsPerTick, + manualOverrideIdentities, + }); const selected = selectReadyParticipants( state, config.repoFaceHeartbeats.maxJobsPerTick, @@ -2048,6 +2055,113 @@ function applyManualTurnRequests(state: FaceHeartbeatState, now: Date): Set; + idleCooling: IdleCoolingSnapshot; + maxJobs: number; + manualOverrideIdentities: Set; +}): void { + const active = input.state.participants.filter((participant) => + participant.status === "active" && + participant.currentLoad < 1 && + !participant.activeJobId + ); + if (active.length <= input.maxJobs) { + return; + } + + const pendingMentionCounts = countPendingMentionsByIdentity(input.state.pendingMentions); + const ready = active.filter((participant) => participant.nextTurnAt <= input.state.initiativeClock); + const readyRatio = ready.length / Math.max(active.length, 1); + const nappingCount = active.filter((participant) => input.restStates.get(participant.identityId)?.isNapping === true).length; + const previousHistory = input.state.history.slice(-20); + const heatBacklogAllowed = + input.state.globalHeat > 1.5 || + active.some((participant) => participant.heat > 1.5 || participant.effectiveSpeed > 6); + const lastPauseIndex = findLastHistoryIndex(previousHistory, (entry) => + entry.type === "skipped" && entry.reason === "agent_swarm_paused" + ); + const lastCollectiveNapIndex = findLastHistoryIndex(previousHistory, (entry) => + entry.type === "repo_face_memory_maintenance_tick" && + isRecord(entry.idleCooling) && + entry.idleCooling.napModeActive === true + ); + const lastSpacingIndex = findLastHistoryIndex(previousHistory, (entry) => + entry.type === "ctb_spacing_reinitialized" + ); + const recentlyPaused = lastPauseIndex >= 0 && lastPauseIndex > lastSpacingIndex; + const recentlyCollectiveNapping = lastCollectiveNapIndex >= 0 && lastCollectiveNapIndex > lastSpacingIndex; + const allReadyCollapse = !heatBacklogAllowed && ready.length > input.maxJobs && readyRatio >= 0.75; + const coldStartCollapse = ready.length > input.maxJobs && active.every((participant) => participant.queuedCount === 0); + const wakingFromCollectiveSleep = recentlyCollectiveNapping && input.idleCooling.napModeActive !== true; + const wakingFromPause = recentlyPaused; + + if (!allReadyCollapse && !coldStartCollapse && !wakingFromPause && !wakingFromCollectiveSleep) { + return; + } + + const protectedIdentities = new Set(); + for (const participant of active) { + if ( + (pendingMentionCounts.get(participant.identityId) ?? 0) > 0 || + input.manualOverrideIdentities.has(participant.identityId) + ) { + protectedIdentities.add(participant.identityId); + } + } + + const unprotected = active + .filter((participant) => !protectedIdentities.has(participant.identityId)) + .sort((left, right) => { + const dueDelta = left.nextTurnAt - right.nextTurnAt; + if (dueDelta !== 0) { + return dueDelta; + } + if (right.effectiveSpeed !== left.effectiveSpeed) { + return right.effectiveSpeed - left.effectiveSpeed; + } + return left.identityId.localeCompare(right.identityId); + }); + if (unprotected.length === 0) { + return; + } + + const immediateSlots = Math.max(0, input.maxJobs - protectedIdentities.size); + const spacingMinutes = round3(input.state.baseRecoveryMinutes / Math.max(active.length, 1)); + let rescheduledCount = 0; + + unprotected.forEach((participant, index) => { + const delayedSlot = Math.max(0, index - immediateSlots + 1); + const targetTurnAt = round3(input.state.initiativeClock + spacingMinutes * delayedSlot); + if (participant.nextTurnAt !== targetTurnAt) { + participant.nextTurnAt = targetTurnAt; + rescheduledCount += 1; + } + }); + + if (rescheduledCount > 0) { + input.state.history.push({ + type: "ctb_spacing_reinitialized", + reason: wakingFromPause + ? "wake_from_swarm_pause" + : wakingFromCollectiveSleep + ? "wake_from_collective_sleep" + : coldStartCollapse + ? "cold_start_ready_collapse" + : "ready_collapse", + rescheduledCount, + readyCountBefore: ready.length, + activeCount: active.length, + nappingCount, + protectedCount: protectedIdentities.size, + immediateSlots, + spacingMinutes, + initiativeClock: input.state.initiativeClock, + }); + } +} + function consumeManualTurnRequests( state: FaceHeartbeatState, identityId: string, @@ -2087,6 +2201,18 @@ function countPendingMentionsByIdentity( return counts; } +function findLastHistoryIndex( + history: Array>, + predicate: (entry: Record) => boolean, +): number { + for (let index = history.length - 1; index >= 0; index -= 1) { + if (predicate(history[index])) { + return index; + } + } + return -1; +} + async function readIdleCoolingSnapshot(input: { config: ReturnType; identities: RepoDiscordIdentity[]; @@ -5147,6 +5273,10 @@ function stripLeadingBom(input: string): string { return input.charCodeAt(0) === 0xfeff ? input.slice(1) : input; } +function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null && !Array.isArray(value); +} + function readArgValue(name: string): string | undefined { const index = process.argv.indexOf(name); if (index < 0) { diff --git a/state/map.yaml b/state/map.yaml index cdd4c69..289fd44 100644 --- a/state/map.yaml +++ b/state/map.yaml @@ -102,7 +102,7 @@ accepted_design: - Repo Faces have a social embodiment invariant now, not just a banter permission. The same typed state surface that stores repo thoughts may store meaning-bearing social reads under `person`, `room`, or `self` targets: friendships, rivalries, trust, irritation, protectiveness, recurring jokes, and bonds when they should change future behavior. Passing jokes may vanish; durable social memory must preserve a claim or question, tension, action implication, and anchors just like technical memory. Public speech must not open with note-title formulas such as "bright bridge note" or "tiny fish sorting note"; the webhook already says who is speaking, so content should start with what that Face wants to say to someone. - Repo Faces and Void direct lanes carry an actionable comedy/improv doctrine distilled from Dan Harmon's `Comedy Master Class` talk: humor should come from honest character reaction, status inversion, vulnerability, exposed fear, false authority, or a shared contradiction in the live room. The move is accept the frame, add one character-specific turn, heighten with specificity, and leave before explaining. Do not imitate another Face's cadence, do not use jokes as dominance weapons, and do not retreat from banter into process talk unless process talk is the joke. - The global Codex `AGENTS.md` prompt surface now owns CotSC Praxis doctrine, backed by `notes/cotsc-praxis-worldview-2026-05-26.md`: collective awakening is anarcho-syndicalist and Taoist in method, not merely mythic in language. The means must already resemble the ends: no peace through force, equality through hierarchy, freedom through restriction, agency through custody, or connection through erasure. Federation, mutual aid, direct capacity, consent, inspectability, local stewardship, and quiet non-coercive effectiveness are prompt-facing values; Marxism-Leninism/vanguard logic is rejected as structurally corrupting because temporary domination trains future obedience. Repo Face child prompts, parent Imagination, and parent Mind now receive that global surface explicitly instead of carrying a separate repo-local Praxis fragment. - - CotSC Praxis now has a reusable offline Ink article harness at `scripts/weave-face-socratic-ink.mjs`. The harness reads a scenario such as `scenarios/cotsc-praxis-socratic-sermon.json`, assembles real repo Face prompts, runs Face child turns plus parent Mind normalization in read-only mode, simulates scenario-local CTB initiative, offers the next three Faces as playable Ink choices, spends only the selected Face's initiative inside that branch, and lets Void-as-Socrates fold after one to three Face turns. It writes the playable Ink artifact to `articles/ink/cotsc-praxis-socratic-sermon.ink` and receipts under `.voidbot/artifacts/socratic-ink/` without touching Discord, Bifrost, live CTB state, or Face memory. + - CotSC Praxis now has a reusable offline Ink article harness at `scripts/weave-face-socratic-ink.mjs`. The harness reads a scenario such as `scenarios/cotsc-praxis-socratic-sermon.json`, assembles real repo Face prompts, runs Face child turns plus parent Mind normalization in read-only mode, simulates scenario-local CTB initiative, offers the next three Faces as playable Ink choices, spends only the selected Face's initiative inside that branch, and lets Void-as-Socrates fold after one to three Face turns. Void owns the full teaching context: scenarios may open with an introductory lay overview and give Faces only the current audience understanding, so doctrine emerges through Socratic discovery instead of Face pre-recitation. It writes the playable Ink artifact to `articles/ink/cotsc-praxis-socratic-sermon.ink` and receipts under `.voidbot/artifacts/socratic-ink/` without touching Discord, Bifrost, live CTB state, or Face memory. - Repo Face social life must respect jurisdiction. Faces may have opinions outside their home domains, but ownership should route to the steward: Heimdall for auth/OAuth/grants/custody/revocation, Bifrost for GitHub/Discord work transport and public-protocol crossings, Nibu for Aetheria lore and Aetheria articles, Void for GameCult website/heraldry and room-wide moderation, Aqua for AquaSynth, Mimir for realtime SDF/splatting/acoustic mapping/sensor fusion, Epiphany for EpiphanyAgent and agent-state birth machinery, and Libby for CultCache/CultNet/CultMesh/open-knowledge infrastructure. Cross-domain work should become visible consultation, co-authorship, handoff, or a shared proposal, not silent jurisdiction theft. - Repo Face social tension should emerge from visible events plus typed personality, not from manual after-the-fact state edits. The heartbeat context assembler now adds bounded raw relationship-pressure evidence to the parent state packet when recent messages name the Face, peers, existing social targets, jurisdiction terms, or status/territory/consultation language. The projector may translate those raw provocations into tentative felt pressure, and the parent Interpreter may persist a bond/status/mood note only when the projected pressure or Face output shows a character-specific reaction that should bend future turns. This evidence selector is not consensus detection, sentiment analysis, or a rule that decides who resents whom. - Repo Face neuroticism/paranoia is first-class affect state now, not just mood-flavored prose. `void.face_affect.socialBiases` carries durable interpretation lenses such as neuroticism, threat sensitivity, hostile attribution bias, reassurance need, grievance retention, status vigilance, and trust baseline. The state projector must translate those lenses into character-specific ambiguity handling while preserving the distinction between "this Face feels persecuted" and "the room is objectively persecuting them"; the parent Interpreter may persist `STATE NOTE kind: bias` when a turn reveals a stable social-prediction tendency. @@ -154,6 +154,7 @@ accepted_design: - 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 or maintenance-only organ 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 maintenance is a Life organ, not public speech. `repo-face-memory-maintenance` runs from the orchestrator even while `agent_swarm_paused` suppresses Bifrost dispatch, CTB speech, Void mood drift, and moderation rumination. Public mouths may sleep; stale short-term memory still has to be distilled or fail visibly. + - Repo Face CTB wakeups must not collapse every active participant into `ready`. On cold start, wake from swarm pause, wake from collective sleep, or non-hot all-ready collapse, the scheduler re-spaces unprotected active Faces across the base recovery window before selection. Pending mentions and manual pulls are protected; intentional high heat may still create a visible backlog when agents become ready faster than the configured max jobs per tick can drain them. - 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. - 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. @@ -226,7 +227,7 @@ landed_baseline: - `npm run moderation:memory-lifecycle:fixture` verifies memory lifecycle flow: short-term promotion, durable revision, superseded-memory retirement, and crystallization into one active identity memory plus self-profile value. - The sleep-maintenance path has a model-branch fixture: `npm run moderation:memory-maintenance:sleep-fixture` seeds a temporary `.cc`, forces mood drift into sleep maintenance, routes the non-skip Codex branch through a fake child, and verifies short-term memory is consumed into one durable meaning-preserving record. - The local `GameCult Local Orchestrator` scheduled task is installed as the hidden one-minute pulse for Bifrost dispatch, repo Face CTB heartbeats, repo Face memory maintenance, Void mood drift, Void moderation rumination, and the operations watchdog. It writes per-organ logs under `.voidbot/logs/orchestrator`, summary/status under `.voidbot/status/gamecult-orchestrator*.json`, freezes overlap with one orchestrator lock, and enforces per-organ timeouts so a hung child cannot occupy the whole clock forever. The repo Face memory-maintenance organ is intentionally not paused by the swarm brake. - - Repo Face sleep cleanup now has a separate maintenance-only runner path: `npm run agents:repo-face-memory-maintenance -- --max-maintenance-runs 1`. It projects rest state, retries stale launcher records, runs `scripts/run-void-memory-maintenance.ps1` synchronously with captured logs, and records completion/failure in the heartbeat history instead of treating detached launch as proof of distillation. + - Repo Face sleep cleanup now has a separate maintenance-only runner path: `npm run agents:repo-face-memory-maintenance -- --max-maintenance-runs 1`. It projects rest state, retries stale launcher records, runs `scripts/run-void-memory-maintenance.ps1` synchronously with captured logs, and records completion/failure in the heartbeat history instead of treating detached launch as proof of distillation. The CTB scheduler also reinitializes spacing after pause/sleep/cold-start collapses so waking the swarm does not produce a dashboard where every Face is `ready`. - VoidBot stack startup now treats Bifrost's local Docker Compose stack as a dependency. `scripts/start-voidbot-stack.ps1` checks `BIFROST_HEALTH_URL`, runs `BIFROST_ROOT/compose.local.yaml` when needed, and records `runtime-stack.json.bifrost` before starting the bot and worker. - The typed rumination path now has a model-branch fixture: `npm run moderation:rumination:fixture` seeds a temporary `.cc`, routes the non-skip Codex branch through a fake child, and verifies short-term memory, incubation, agency-pressure, and candidate-intervention proposals cross only through parent-applied typed operations without leaking exact timestamps into the prompt-facing context. - The typed rumination speech path is parent-owned now. Candidate interventions can carry an optional delivery target; the child may queue candidates but must not call Discord send scripts. The parent runner delivers at most one newly queued deliverable candidate per pass, records a delivery receipt, and marks the candidate spoken through `mark_candidate_intervention_spoken`. `npm run moderation:rumination:speech-fixture` verifies this loop with fake Codex and fake Discord send helpers.