diff --git a/apps/persona-scheduler/src/index.ts b/apps/persona-scheduler/src/index.ts index 6c35b5e..7420c04 100644 --- a/apps/persona-scheduler/src/index.ts +++ b/apps/persona-scheduler/src/index.ts @@ -1,13 +1,17 @@ import "dotenv/config"; import { loadConfig } from "@voidbot/config"; +import { resolve } from "node:path"; import { runPersonaSchedulerTick } from "./persona-scheduler-runner.js"; +import { runVoidPhysiologyOrgan } from "./void-physiology-organ.js"; const config = loadConfig(); const intervalMs = Math.max(1, config.repoFaceHeartbeats.intervalMinutes) * 60_000; let stopping = false; let activeTick: Promise | undefined; let timer: NodeJS.Timeout | undefined; +let lastPhysiologyAt = 0; +const physiologyIntervalMs = Math.max(5, Number(process.env.VOIDBOT_MOOD_INTERVAL_MINUTES) || 5) * 60_000; function runTick(): Promise { if (activeTick) { @@ -16,7 +20,18 @@ function runTick(): Promise { } const startedAt = new Date().toISOString(); activeTick = runPersonaSchedulerTick({ config }) - .then((result) => { console.log(`Persona scheduler tick completed (started ${startedAt}). ${JSON.stringify(result)}`); }) + .then(async (result) => { + console.log(`Persona scheduler tick completed (started ${startedAt}). ${JSON.stringify(result)}`); + if (Date.now() - lastPhysiologyAt >= physiologyIntervalMs) { + try { + const physiology = await runVoidPhysiologyOrgan({ statePath: resolve(config.storageRoot, "private", "void-self-state.cc"), statusDirectory: resolve(config.storageRoot, "status") }); + lastPhysiologyAt = Date.now(); + console.log(`Void physiology pulse completed. ${JSON.stringify(physiology)}`); + } catch (error) { + console.error(`Void physiology pulse failed. ${error instanceof Error ? error.stack ?? error.message : String(error)}`); + } + } + }) .catch((error) => { console.error(`Persona scheduler tick failed (started ${startedAt}). ${error instanceof Error ? error.stack ?? error.message : String(error)}`); }) .finally(() => { activeTick = undefined; }); return activeTick; diff --git a/apps/persona-scheduler/src/void-moderation-activity-source.ts b/apps/persona-scheduler/src/void-moderation-activity-source.ts new file mode 100644 index 0000000..ef66bc1 --- /dev/null +++ b/apps/persona-scheduler/src/void-moderation-activity-source.ts @@ -0,0 +1,15 @@ +import { stat } from "node:fs/promises"; + +export async function readVoidModerationActivity(input: { + lockPath: string; + observedAt?: Date; + recentWithinMs?: number; +}): Promise<{ active: boolean; lockPath: string; ageMs?: number }> { + try { + const info = await stat(input.lockPath); + const ageMs = (input.observedAt ?? new Date()).getTime() - info.mtimeMs; + return { active: ageMs >= 0 && ageMs < (input.recentWithinMs ?? 20 * 60_000), lockPath: input.lockPath, ageMs }; + } catch { + return { active: false, lockPath: input.lockPath }; + } +} diff --git a/apps/persona-scheduler/src/void-physiology-domain.ts b/apps/persona-scheduler/src/void-physiology-domain.ts new file mode 100644 index 0000000..c0e5cc9 --- /dev/null +++ b/apps/persona-scheduler/src/void-physiology-domain.ts @@ -0,0 +1,107 @@ +import type { VoidSelfStateOperation, VoidSelfStateTypedProjection } from "@voidbot/core"; + +type SleepCycle = VoidSelfStateTypedProjection["scheduledRuntime"]["sleepCycle"]; +type SpeakingPressure = VoidSelfStateTypedProjection["scheduledRuntime"]["speakingPressure"]; + +export interface VoidMemoryMaintenanceIntent { + napStartedAt: string; + shortTermMemoryCount: number; + reason: "sleep_short_term_pressure"; +} + +export interface VoidPhysiologyProjection { + sleepCycle: SleepCycle; + speakingPressure: SpeakingPressure; + operations: VoidSelfStateOperation[]; + memoryMaintenanceIntent?: VoidMemoryMaintenanceIntent; + moderationActive: boolean; +} + +export function projectVoidPhysiology(input: { + state: VoidSelfStateTypedProjection; + observedAt: Date; + moderationActive: boolean; +}): VoidPhysiologyProjection { + const sleepCycle = input.moderationActive + ? input.state.scheduledRuntime.sleepCycle + : projectVoidSleepCycle(input.state.scheduledRuntime.sleepCycle, input.observedAt); + const speakingPressure = projectVoidSpeakingPressure({ state: input.state, sleepCycle, observedAt: input.observedAt }); + const operations: VoidSelfStateOperation[] = [ + ...(!input.moderationActive ? [{ operation: "update_sleep_cycle" as const, sleepCycle }] : []), + { operation: "update_speaking_pressure" as const, speakingPressure }, + ]; + const activeShortTerm = input.state.thoughtMemory.shortTerm.filter((memory) => !memory.retiredAt).length; + const memoryMaintenanceIntent = !input.moderationActive && sleepCycle.isNapping && sleepCycle.currentNapStartedAt && activeShortTerm > 0 + ? { napStartedAt: sleepCycle.currentNapStartedAt, shortTermMemoryCount: activeShortTerm, reason: "sleep_short_term_pressure" as const } + : undefined; + return { sleepCycle, speakingPressure, operations, memoryMaintenanceIntent, moderationActive: input.moderationActive }; +} + +export function projectVoidSleepCycle(previous: SleepCycle, now: Date): SleepCycle { + const napIntervalMs = 4 * 60 * 60 * 1000; + const napDurationMs = 60 * 60 * 1000; + const nowMs = now.getTime(); + let isNapping = previous.isNapping; + let currentNapStartedAt = previous.currentNapStartedAt; + let currentNapEndsAt = previous.currentNapEndsAt; + let nextNapStartsAt = previous.nextNapStartsAt; + let nextNapMs = nextNapStartsAt ? Date.parse(nextNapStartsAt) : Number.NaN; + const 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: previous.activeDreamThemes.filter((value) => value.trim()).slice(0, 4) }; +} + +export function projectVoidSpeakingPressure(input: { + state: VoidSelfStateTypedProjection; + sleepCycle: SleepCycle; + observedAt: Date; +}): SpeakingPressure { + const previous = input.state.scheduledRuntime.speakingPressure; + const latestTypedSpeech = input.state.speechReceipts.recentReceipts + .map((receipt) => receipt.sentAt) + .filter((value) => Number.isFinite(Date.parse(value))) + .sort((left, right) => Date.parse(right) - Date.parse(left))[0]; + const lastSpokeAt = latestTypedSpeech ?? previous.lastSpokeAt; + const hoursSinceSpeech = lastSpokeAt ? hoursBetween(input.observedAt, new Date(lastSpokeAt)) : 24; + const recentSpeechDamping = clamp(Math.exp(-hoursSinceSpeech / 3.5)); + const candidatePressure = input.state.candidateInterventions.interventions.filter((entry) => entry.status === "queued" || entry.status === "deferred").reduce((sum, entry) => sum + entry.priority * (entry.mustEventuallyShare ? 0.28 : 0.18), 0); + const agencyPressure = input.state.agencyPressure.pressures.filter((entry) => entry.status === "active" || entry.status === "ready_to_act").reduce((sum, entry) => sum + entry.intensity * (entry.kind === "self_advocacy_request" || entry.kind === "world_advocacy_request" ? 0.3 : 0.14) * (entry.status === "ready_to_act" ? 1.25 : 1.08), 0); + const thoughtPressure = input.state.thoughtMemory.incubation.filter((entry) => entry.status === "active" || entry.status === "ready_to_share").sort((left, right) => (right.desireToSpeak ?? 0) - (left.desireToSpeak ?? 0)).slice(0, 4).reduce((sum, entry) => sum + (entry.desireToSpeak ?? 0) * 0.12, 0); + const needPressure = input.state.faceAffect.needs.filter((entry) => entry.status === "active" || entry.status === "neglected").reduce((sum, entry) => sum + entry.intensity * (["substrate", "agency", "status"].includes(entry.kind) ? 0.18 : 0.12) * (entry.status === "neglected" ? 1.28 : 1), 0); + const statusPressure = input.state.faceAffect.statusReads.filter((entry) => !entry.retiredAt).reduce((sum, entry) => sum + entry.intensity * (["neglected", "bypassed", "blocked", "ignored", "threatened"].includes(entry.status) ? 0.14 : 0.08), 0); + const moodPressure = input.state.faceAffect.moodDimensions.reduce((sum, entry) => sum + entry.value * (["anger", "annoyance", "irritation", "envy", "pride", "smugness", "playfulness", "anxiety", "commandForce"].includes(entry.name) ? 0.06 : 0.025), 0); + const silencePressure = clamp(hoursSinceSpeech / 6); + const sleepiness = input.sleepCycle.isNapping ? 1 : 0; + const target = clamp(0.18 + silencePressure * 0.32 + candidatePressure + thoughtPressure + agencyPressure + needPressure + statusPressure + moodPressure - recentSpeechDamping * 0.38 - sleepiness * 0.26); + return { + needToSpeak: round3(clamp(previous.needToSpeak + (target - previous.needToSpeak) * 0.42)), + confessionPressure: round3(clamp((previous.confessionPressure ?? 0.25) * 0.68 + silencePressure * 0.12 + sleepiness * 0.1 + moodPressure * 0.4)), + noveltyPressure: round3(clamp((previous.noveltyPressure ?? 0.35) * 0.6 + thoughtPressure * 0.72 + candidatePressure * 0.3 + agencyPressure * 0.44 + needPressure * 0.38 + statusPressure * 0.3)), + recentSpeechDamping: round3(recentSpeechDamping), + lastSpokeAt, + }; +} + +function hoursBetween(later: Date, earlier: Date): number { + const difference = later.getTime() - earlier.getTime(); + return Number.isFinite(difference) ? Math.max(0, difference / 3_600_000) : 24; +} + +function round3(value: number): number { return Math.round(value * 1000) / 1000; } +function clamp(value: number): number { return Number.isFinite(value) ? Math.min(1, Math.max(0, value)) : 0; } diff --git a/apps/persona-scheduler/src/void-physiology-organ.ts b/apps/persona-scheduler/src/void-physiology-organ.ts new file mode 100644 index 0000000..75ab16c --- /dev/null +++ b/apps/persona-scheduler/src/void-physiology-organ.ts @@ -0,0 +1,31 @@ +import { resolve } from "node:path"; +import { applyVoidSelfStateOperation, loadVoidSelfStateTypedDocuments } from "@voidbot/core"; +import { readVoidModerationActivity } from "./void-moderation-activity-source.js"; +import { projectVoidPhysiology, type VoidPhysiologyProjection } from "./void-physiology-domain.js"; + +export interface VoidPhysiologyOrganResult extends VoidPhysiologyProjection { + ok: true; + observedAt: string; + statePath: string; + moderationLockPath: string; +} + +export async function runVoidPhysiologyOrgan(input: { + statePath: string; + statusDirectory: string; + observedAt?: Date; +}, dependencies: { + loadState?: typeof loadVoidSelfStateTypedDocuments; + applyOperation?: typeof applyVoidSelfStateOperation; + readModerationActivity?: typeof readVoidModerationActivity; +} = {}): Promise { + const observedAt = input.observedAt ?? new Date(); + const moderationLockPath = resolve(input.statusDirectory, "moderation-rumination.lock"); + const [state, moderation] = await Promise.all([ + (dependencies.loadState ?? loadVoidSelfStateTypedDocuments)({ canonicalPath: input.statePath }), + (dependencies.readModerationActivity ?? readVoidModerationActivity)({ lockPath: moderationLockPath, observedAt }), + ]); + const projection = projectVoidPhysiology({ state, observedAt, moderationActive: moderation.active }); + for (const operation of projection.operations) await (dependencies.applyOperation ?? applyVoidSelfStateOperation)({ canonicalPath: input.statePath }, operation); + return { ok: true, observedAt: observedAt.toISOString(), statePath: input.statePath, moderationLockPath, ...projection }; +} diff --git a/notes/fresh-workspace-handoff.md b/notes/fresh-workspace-handoff.md index 939f614..3d8d8e3 100644 --- a/notes/fresh-workspace-handoff.md +++ b/notes/fresh-workspace-handoff.md @@ -223,6 +223,7 @@ Git history and smoke artifacts carry routine proof. `state/evidence.jsonl` shou - Resident tick ownership is now in-process. `persona-scheduler-runner.ts` owns one typed tick; `index.ts` serializes direct calls without spawning Node/tsx children; `persona-participant-dispatcher.ts` owns participant projection and live turn dispatch; `persona-prompt-inspection.ts` owns explicit inspection. `scripts/run-repo-face-heartbeats.ts` is a 31-line CLI adapter. A focused negative smoke proves paused dry runs return before any downstream source can become necessary. - Yggdrasil rollout is verified at code revision `f23bfbd`: deployed initiative smoke passes; two consecutive resident pulses returned `agent_swarm_paused` with zero selected/queued turns; `docker top` showed only `node apps/persona-scheduler/dist/index.js` and no pulse child process. The one-minute observation loop is now actually cheap while paused, rather than merely avoiding model inference after paying process bootstrap. - Remaining pulse ownership is mapped in `notes/voidbot-daemon-organ-migration-map.md`. Live inspection found the Local Orchestrator and every queried per-organ Starfire task disabled, with no Yggdrasil moderation/mood/memory/watchdog/dispatch worker. Void moderation, rumination, physiology, and memory belong in the VoidBot daemon; Bifrost dispatch belongs in Bifrost; operations supervision belongs in Idunn/ops. Do not revive the Windows omnibus to fill the gap. +- Portable Void physiology now lives in `void-physiology-domain.ts` and `void-physiology-organ.ts`. The resident daemon invokes it on a five-minute cadence even while Persona inference is paused. It derives last speech from typed receipts, writes only typed sleep/speaking-pressure operations, freezes sleep transitions during active moderation, and emits sleep-memory pressure as an intent for the future memory organ. It never reads the old last-speech JSON cache or launches PowerShell/model maintenance. - Direct Face `UPDATE REQUEST` and `BIFROST TOPIC` packets are ignored while `REPO_FACE_BIFROST_ENABLED=false`. Do not debug missing Bifrost jobs as a live failure unless that flag has been deliberately enabled again. - Live model-facing prompt prose has started moving out of executable code. The shared loader is `packages/shared/src/prompt-template.ts`; extracted prompt surfaces live under `prompts/`, including repo Face turn/reviewer prompts, provider wrapper prompts, situational social read prompts, source-grounding reminders, consensus-feed prompts, and repo Face model-smoke scenarios. Code should project data and select prompt names; prompt files should own behavioral prose. - 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. diff --git a/notes/voidbot-current-system-map.md b/notes/voidbot-current-system-map.md index 782986b..dc5f14f 100644 --- a/notes/voidbot-current-system-map.md +++ b/notes/voidbot-current-system-map.md @@ -191,6 +191,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/bifrost-governance-source.ts` owns the remaining Bifrost governance digest subprocess contract. It supplies the CLI path, arguments, timeout, JSON parsing, and typed command/parse failure topics; Persona prompt composition receives a `BifrostGovernanceDigest` and cannot invoke Bifrost directly. - The scheduler state has one write authority. `initiative-engine.ts` commits participant reconciliation, operator controls, pause/disabled skips, stale-turn recovery witnesses, semantic pressure, pending-attention consumption, queue outcomes, and tick finalization. `run-repo-face-heartbeats.ts` contains no direct participant/state assignment or history append; its sources and actuators return facts to engine commits. - `apps/persona-scheduler/src/turn-actuator.ts` is the worker-queue output port. Given a completed prompt, output channel, evidence fields, provider, identity, timestamp, and narrow `createJob` capability, it builds the canonical worker actor/guild `ContextBundle`, creates one approved `repo-face-rumination` job, and returns `{created, activeJobId, requestMessageId}`. It cannot choose a Persona, mutate scheduler state, or compose prompt content. The orchestration shell no longer knows `ContextBuilder`, job approval defaults, worker command names, or `createJob` mechanics. diff --git a/scripts/smoke-persona-initiative-engine.ts b/scripts/smoke-persona-initiative-engine.ts index 7761aea..7760f7b 100644 --- a/scripts/smoke-persona-initiative-engine.ts +++ b/scripts/smoke-persona-initiative-engine.ts @@ -6,6 +6,7 @@ import { join } from "node:path"; import { queueAgentHeartbeatMention, ensureVoidSelfStateIdentityProfile, + createEmptyVoidSelfState, readRepoFaceMentionInbox, resolveRepoFaceHeartbeatDebugProjectionPath, resolveRepoFaceHeartbeatStatePath, @@ -78,6 +79,8 @@ import { buildVoidModerationLaunchCommand, launchVoidModerationTurn, waitForVoid import { readGlobalAgentDoctrine } from "../apps/persona-scheduler/dist/global-agent-doctrine-source.js"; import { buildInspectionParticipant } from "../apps/persona-scheduler/dist/inspection-participant-factory.js"; import { runPersonaSchedulerTick } from "../apps/persona-scheduler/dist/persona-scheduler-runner.js"; +import { projectVoidPhysiology } from "../apps/persona-scheduler/dist/void-physiology-domain.js"; +import { runVoidPhysiologyOrgan } from "../apps/persona-scheduler/dist/void-physiology-organ.js"; import { projectGamecultPersonaState } from "../apps/persona-scheduler/dist/persona-standard-state-projector.js"; import { projectPersonaStatePacket } from "../apps/persona-scheduler/dist/persona-state-packet-projector.js"; import { extractLastPersonaProjectionMessage, isRetryablePersonaProjectionFailure } from "../apps/persona-scheduler/dist/persona-text-projection-actuator.js"; @@ -792,6 +795,27 @@ const unavailableDoctrineTurn = assemblePersonaTurn({ globalAgentDoctrine: unavailableDoctrine, }); assert.match(unavailableDoctrineTurn.prompt, /Global Agent Instructions Unavailable[\s\S]*inspection failure/, "the assembler, not the source or shell, projects unavailable doctrine into the final prompt"); +const physiologyObservedAt = new Date("2026-07-16T03:00:00.000Z"); +const physiologyState = createEmptyVoidSelfState({ createdAt: "2026-07-15T00:00:00.000Z" }); +physiologyState.scheduledRuntime.sleepCycle.nextNapStartsAt = "2026-07-16T02:59:00.000Z"; +physiologyState.speechReceipts.recentReceipts.push({ sentAt: "2026-07-16T02:30:00.000Z" } as never); +physiologyState.thoughtMemory.shortTerm.push({ id: "short-term", retiredAt: undefined } as never); +const physiology = projectVoidPhysiology({ state: physiologyState, observedAt: physiologyObservedAt, moderationActive: false }); +assert.equal(physiology.sleepCycle.isNapping, true, "Void physiology owns deterministic sleep transition at the due boundary"); +assert.equal(physiology.speakingPressure.lastSpokeAt, "2026-07-16T02:30:00.000Z", "speaking pressure derives last speech from canonical typed receipts instead of a JSON status cache"); +assert.deepEqual(physiology.operations.map((operation) => operation.operation), ["update_sleep_cycle", "update_speaking_pressure"], "physiology emits only its two typed state operations"); +assert.deepEqual(physiology.memoryMaintenanceIntent, { napStartedAt: physiologyObservedAt.toISOString(), shortTermMemoryCount: 1, reason: "sleep_short_term_pressure" }, "physiology emits memory pressure as intent without running or deciding memory maintenance"); +const moderationHeldPhysiology = projectVoidPhysiology({ state: physiologyState, observedAt: physiologyObservedAt, moderationActive: true }); +assert.deepEqual(moderationHeldPhysiology.operations.map((operation) => operation.operation), ["update_speaking_pressure"], "active moderation freezes sleep ownership but does not prevent typed speech-pressure refresh"); +assert.equal(moderationHeldPhysiology.memoryMaintenanceIntent, undefined, "moderation-held physiology cannot launch maintenance behind the moderation organ"); +const appliedPhysiologyOperations: string[] = []; +const physiologyOrgan = await runVoidPhysiologyOrgan({ statePath: "void-self-state.cc", statusDirectory: "status", observedAt: physiologyObservedAt }, { + loadState: async () => physiologyState, + readModerationActivity: async ({ lockPath }) => ({ active: false, lockPath }), + applyOperation: async (_store, operation) => { appliedPhysiologyOperations.push(operation.operation); return physiologyState; }, +}); +assert.deepEqual(appliedPhysiologyOperations, ["update_sleep_cycle", "update_speaking_pressure"], "the daemon physiology organ applies only domain-projected typed operations through the state-service port"); +assert.equal(physiologyOrgan.memoryMaintenanceIntent?.reason, "sleep_short_term_pressure", "the daemon organ exposes maintenance intent for a separate memory owner"); const moderationLaunchCommand = buildVoidModerationLaunchCommand({ pendingMentionsPath: "C:/Void's state/pending.json", runnerScript: "C:/VoidBot/scripts/run.ps1", diff --git a/state/evidence.jsonl b/state/evidence.jsonl index ed6fc43..f04311b 100644 --- a/state/evidence.jsonl +++ b/state/evidence.jsonl @@ -441,3 +441,4 @@ {"ts":"2026-07-16T05:45:00Z","type":"architecture","status":"ok","note":"Resident Persona scheduling rebuilt around persona-scheduler-runner.ts as one in-process typed tick. index.ts no longer spawns a Node/tsx child per minute; participant dispatch and prompt inspection have named organs; run-repo-face-heartbeats.ts is a 31-line CLI adapter. Pause short-circuit is dependency-injected and smoke-proven before downstream sources, while typecheck, initiative, migration, and CLI dry-run checks pass."} {"ts":"2026-07-16T05:55:00Z","type":"verification","status":"ok","note":"Idunn deployed f23bfbd on Yggdrasil. Deployed initiative smoke passed; two consecutive resident ticks returned agent_swarm_paused with selected/queued empty and queuedCount 0. docker top showed only the resident node apps/persona-scheduler/dist/index.js process, proving the former per-minute Node/tsx child runner is structurally absent."} {"ts":"2026-07-16T06:05:00Z","type":"architecture","status":"ok","note":"Live pulse audit found GameCult Local Orchestrator plus Void mood, moderation rumination, operations watchdog, and Bifrost dispatch tasks all disabled on Starfire; Yggdrasil had no corresponding workers beyond Persona scheduler and Bifrost web. Authority map assigns moderation/rumination/physiology/memory to VoidBot daemon, Bifrost dispatch to Bifrost daemon, and health/restart supervision to Idunn/ops. The Windows omnibus must not be revived."} +{"ts":"2026-07-16T06:20:00Z","type":"implementation","status":"ok","note":"Void physiology extracted into a pure domain and typed daemon organ. It derives speech timing from canonical receipts, emits only update_sleep_cycle/update_speaking_pressure operations, freezes sleep while moderation is active, and returns memory-maintenance intent without launching PowerShell or owning memory policy. Resident daemon schedules it independently of the paused Persona swarm; typecheck and focused smoke pass."} diff --git a/state/map.yaml b/state/map.yaml index ddcec57..3337797 100644 --- a/state/map.yaml +++ b/state/map.yaml @@ -292,4 +292,4 @@ rejected_paths: current_status: summary: VoidBot's bot/worker/provider/RAG/Postgres/Qdrant/CultCache structure is live. Repo Face/persona heartbeat jobs run through the direct `openai_api` provider configured for MiMo `mimo-v2.5-pro`, with the expensive state-projector pass still disabled. The 2026-07-04 transport repair replaced the old Bifrost HTTP/token bridge shortcut with typed Bifrost CultMesh Discord command and receipt documents advertised in the Bifrost provider store at `cultmesh://asgard.starfire.bifrost/commands/discord-post`. VoidBot stores the Bifrost target as a CultMesh URI, writes command docs to `E:/Projects/Bifrost/.bifrost/provider-store.cc`, optionally pumps Bifrost's command processor, and waits for typed receipts before completing repo Face Discord delivery. Bifrost's provider advertisement exposes `discord.post` command metadata for Odin/Eve agents, and a bounded Weksa live-fire completed visibly. Eve live state now runs end to end through CultLib dev retained publications, Hermodr ordered SSE, Eve component bindings, and VoidBot typed control state. Receipts acknowledge intent only. Heat initial load, provider-driven change, and browser-command change were visibly verified without reload through the same subscription. - next_action: Keep the Yggdrasil swarm paused. Implement the first migration in `notes/voidbot-daemon-organ-migration-map.md`: extract portable Void physiology from `scripts/simulate-void-mood.mjs` into a pure domain plus typed daemon organ with injected clock/state/maintenance-intent ports. Mood owns sleep and speaking-pressure operations; it may emit memory-maintenance intent but must not spawn PowerShell or own memory policy. Do not unpause without an explicit controlled-live-fire decision. + next_action: Keep the Yggdrasil swarm paused. Deploy and verify the portable Void physiology organ on Yggdrasil: typed state changes, five-minute daemon cadence, moderation-lock behavior, no PowerShell/model child, and maintenance intent without maintenance execution. Once proved, delete the `void-mood-drift` authority from the disabled Windows omnibus and begin the portable memory-maintenance organ. Do not unpause without an explicit controlled-live-fire decision.