From e5c2a5ff15a3dd16aa4b9a5cef94c04a4aacfbf6 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 16 Jul 2026 05:42:55 +0100 Subject: [PATCH] Unify Discord delivery through Bifrost --- .env.example | 1 + .../src/bifrost-discord-delivery-actuator.ts | 28 ++ apps/persona-scheduler/src/index.ts | 20 ++ apps/worker/src/index.ts | 301 +----------------- notes/fresh-workspace-handoff.md | 1 + notes/voidbot-current-system-map.md | 1 + notes/voidbot-daemon-organ-migration-map.md | 5 +- packages/config/src/index.ts | 9 + packages/core/src/bifrost-discord-command.ts | 120 +++++++ packages/core/src/index.ts | 1 + scripts/smoke-persona-initiative-engine.ts | 32 ++ state/evidence.jsonl | 1 + state/map.yaml | 4 +- 13 files changed, 231 insertions(+), 293 deletions(-) create mode 100644 apps/persona-scheduler/src/bifrost-discord-delivery-actuator.ts create mode 100644 packages/core/src/bifrost-discord-command.ts diff --git a/.env.example b/.env.example index 2c45de6..e70a97d 100644 --- a/.env.example +++ b/.env.example @@ -16,6 +16,7 @@ DISCORD_PERSONA_AVATAR_URL= VOID_PUBLIC_ROOM_CHANNEL_ID= VOID_PUBLIC_ROOM_PERSONA_NAME=Void VOID_PUBLIC_ROOM_PERSONA_AVATAR_URL= +VOID_CANDIDATE_DELIVERY_ENABLED=false STATE_STORAGE_BACKEND=postgres DATABASE_DSN=postgres://voidbot:voidbot@localhost:5432/voidbot # Leave this as local_json until Qdrant is running and you've migrated the existing vectors. diff --git a/apps/persona-scheduler/src/bifrost-discord-delivery-actuator.ts b/apps/persona-scheduler/src/bifrost-discord-delivery-actuator.ts new file mode 100644 index 0000000..ad5904d --- /dev/null +++ b/apps/persona-scheduler/src/bifrost-discord-delivery-actuator.ts @@ -0,0 +1,28 @@ +import { postDiscordViaBifrostCultMesh, type BifrostDiscordCommandConfig } from "@voidbot/core"; + +export interface VoidBifrostDeliveryConfig extends BifrostDiscordCommandConfig {} + +export async function deliverVoidCandidateViaBifrost(input: { + candidateId: string; + channelId: string; + replyToMessageId?: string; + content: string; + personaName: string; + personaAvatarUrl?: string; +}, config: VoidBifrostDeliveryConfig): Promise<{ messageId: string; transport: string }> { + const receipt = await postDiscordViaBifrostCultMesh({ + idempotencyKey: JSON.stringify({ + candidateId: input.candidateId, + channelId: input.channelId, + replyToMessageId: input.replyToMessageId ?? "", + content: input.content, + }), + source: { kind: "voidbot.void.candidate", id: input.candidateId }, + actor: { id: "void", displayName: input.personaName }, + channelId: input.channelId, + content: input.content, + replyToMessageId: input.replyToMessageId, + personaAvatarUrl: input.personaAvatarUrl, + }, config); + return { messageId: receipt.messageId, transport: receipt.transport }; +} diff --git a/apps/persona-scheduler/src/index.ts b/apps/persona-scheduler/src/index.ts index d05c52e..e8cb72c 100644 --- a/apps/persona-scheduler/src/index.ts +++ b/apps/persona-scheduler/src/index.ts @@ -10,6 +10,8 @@ import { projectVoidMemoryOperations } from "./void-memory-text-actuator.js"; import { runVoidModerationHeartbeat } from "./void-moderation-heartbeat-organ.js"; import { projectVoidModerationOperations } from "./void-moderation-text-actuator.js"; import { runVoidModerationEnforcement } from "./void-moderation-enforcement-organ.js"; +import { runVoidCandidateDelivery } from "./void-candidate-delivery-organ.js"; +import { deliverVoidCandidateViaBifrost } from "./bifrost-discord-delivery-actuator.js"; const config = loadConfig(); const intervalMs = Math.max(1, config.repoFaceHeartbeats.intervalMinutes) * 60_000; @@ -73,6 +75,24 @@ function runTick(): Promise { console.error(`Void moderation-enforcement pulse failed; pending cases remain typed debt for the next cadence. ${error instanceof Error ? error.stack ?? error.message : String(error)}`); } } + if (config.voidCandidateDelivery.enabled) { + try { + const delivery = await runVoidCandidateDelivery({ + statePath: resolve(config.storageRoot, "private", "void-self-state.cc"), + personaName: config.voidCandidateDelivery.personaName, + personaAvatarUrl: config.voidCandidateDelivery.personaAvatarUrl, + }, { + deliver: (candidate) => deliverVoidCandidateViaBifrost(candidate, { + ...config.bifrostCultMesh, + bifrostRoot: config.bifrostRoot, + cultlibRoot: process.env.VOIDBOT_CULTLIB_ROOT, + }), + }); + if (delivery.status === "ok") console.log(`Void candidate delivery completed. ${JSON.stringify(delivery)}`); + } catch (error) { + console.error(`Void candidate delivery failed; the queued candidate remains typed debt. ${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; }); diff --git a/apps/worker/src/index.ts b/apps/worker/src/index.ts index 3c5e07e..00360e8 100644 --- a/apps/worker/src/index.ts +++ b/apps/worker/src/index.ts @@ -1,7 +1,6 @@ import "dotenv/config"; import { createHash } from "node:crypto"; -import { createRequire } from "node:module"; import { mkdir, writeFile } from "node:fs/promises"; import { spawnSync } from "node:child_process"; import { isAbsolute, join, relative, resolve } from "node:path"; @@ -14,6 +13,7 @@ import { buildVoidMcpServerConfig, createStateStorage, applyVoidSelfStateOperation, + postDiscordViaBifrostCultMesh, findRepoDiscordIdentity, faceRegistryAsRepoDiscordRegistry, hasFreshHumanRepoFaceVoiceListener, @@ -81,10 +81,6 @@ const REPO_IDENTITY_PROPOSAL_PR_SENTINEL = "VOIDBOT_REPO_IDENTITY_PROPOSAL_PR:"; const REPO_IDENTITY_PR_COMMENT_SENTINEL = "VOIDBOT_REPO_IDENTITY_PR_COMMENT:"; const REPO_IDENTITY_UPDATE_REQUEST_SENTINEL = "VOIDBOT_REPO_IDENTITY_UPDATE_REQUEST:"; const REPO_IDENTITY_BIFROST_TOPIC_SENTINEL = "VOIDBOT_REPO_IDENTITY_BIFROST_TOPIC:"; -const BIFROST_DISCORD_POST_COMMAND_DOCUMENT_TYPE = "bifrost.bridge.discord_post_command"; -const BIFROST_DISCORD_POST_COMMAND_SCHEMA_ID = "bifrost.bridge.discord_post_command.v1"; -const BIFROST_DISCORD_POST_RECEIPT_DOCUMENT_TYPE = "bifrost.bridge.discord_post_receipt"; -const BIFROST_DISCORD_POST_RECEIPT_SCHEMA_ID = "bifrost.bridge.discord_post_receipt.v1"; const CURRENT_REPO_FACE_IDENTITY_SELECTORS = new Set([ "face_id", "faceid", @@ -1715,296 +1711,23 @@ async function postRepoIdentityDiscordViaCultMeshCommand(input: { content: string; replyToMessageId?: string; }): Promise { - assertBifrostCultMeshCommandUri(config.bifrostCultMesh.commandUri); - const commandId = buildBifrostDiscordCommandId(input); - const node = await openBifrostCommandNode(config.bifrostCultMesh.storePath); - const requestedAt = new Date().toISOString(); - const command = { - schemaName: BIFROST_DISCORD_POST_COMMAND_DOCUMENT_TYPE, - schemaVersion: BIFROST_DISCORD_POST_COMMAND_SCHEMA_ID, - commandId, - command: "discord-post", - status: "pending", - requestedBy: "voidbot", - requestedAt, - updatedAt: requestedAt, - commandUri: config.bifrostCultMesh.commandUri, - source: { - kind: "voidbot.repo-face.say", - id: input.job.id, - jobId: input.job.id, - requestMessageId: input.job.requestMessageId, - }, - actor: { - id: input.identity.id, - displayName: input.identity.displayName, - repoName: input.identity.repoName, - }, - payload: { - identityId: input.identity.id, - channelId: input.channelId, - content: input.content, - personaName: input.identity.displayName, - personaAvatarUrl: input.identity.avatarUrl ?? "", - replyToMessageId: input.replyToMessageId ?? "", - }, - }; - - await node.put(bifrostDiscordCommandDefinition(), commandId, command); - await node.flush?.(); - - if (config.bifrostCultMesh.pumpEnabled) { - pumpBifrostCultMeshCommand(commandId); - } - - const receipt = await waitForBifrostDiscordReceipt(node, commandId, config.bifrostCultMesh.timeoutMs); - if (receipt.status !== "completed" || receipt.ok !== true) { - throw new Error( - `Bifrost CultMesh Discord command ${commandId} failed: ${bifrostString(receipt.error) || "no error detail"}`, - ); - } - const messageId = bifrostString(receipt.messageId); - const transport = bifrostString(receipt.transport); - if (!messageId || !transport) { - throw new Error(`Bifrost CultMesh Discord command ${commandId} returned an incomplete receipt.`); - } + const receipt = await postDiscordViaBifrostCultMesh({ + idempotencyKey: JSON.stringify({ jobId: input.job.id, identityId: input.identity.id, channelId: input.channelId, replyToMessageId: input.replyToMessageId ?? "", content: input.content }), + source: { kind: "voidbot.repo-face.say", id: input.job.id, jobId: input.job.id, requestMessageId: input.job.requestMessageId }, + actor: { id: input.identity.id, displayName: input.identity.displayName, repoName: input.identity.repoName }, + channelId: input.channelId, content: input.content, replyToMessageId: input.replyToMessageId, personaAvatarUrl: input.identity.avatarUrl, + }, { + ...config.bifrostCultMesh, bifrostRoot: config.bifrostRoot, cultlibRoot: process.env.VOIDBOT_CULTLIB_ROOT, + }); return { action: "discord-post", ok: true, - messageId, - transport: transport === "bot" || transport === "webhook" ? transport : "webhook", - url: bifrostString(receipt.url), + messageId: receipt.messageId, + transport: receipt.transport === "bot" || receipt.transport === "webhook" ? receipt.transport : "webhook", + url: receipt.url, }; } -function buildBifrostDiscordCommandId(input: { - job: JobRecord; - identity: RepoDiscordIdentity; - channelId: string; - content: string; - replyToMessageId?: string; -}): string { - const hash = createHash("sha1") - .update(JSON.stringify({ - jobId: input.job.id, - identityId: input.identity.id, - channelId: input.channelId, - replyToMessageId: input.replyToMessageId ?? "", - content: input.content, - })) - .digest("hex") - .slice(0, 20); - return `voidbot-discord-${hash}`; -} - -function assertBifrostCultMeshCommandUri(value: string): void { - if (!/^cultmesh:\/\/[^/]+\/commands\/discord-post(?:$|[/?#])/.test(value)) { - throw new Error( - `BIFROST_CULTMESH_COMMAND_URI must be a CultMesh Discord command URI, got "${value}".`, - ); - } -} - -async function openBifrostCommandNode(storePath: string): Promise<{ - put: (definition: unknown, key: string, value: unknown) => Promise; - get: (definition: unknown, key: string) => unknown; - flush?: () => Promise; - cache?: { pullAllBackingStores?: () => Promise }; -}> { - const { CultMesh } = loadBifrostCultMeshRuntime(); - return CultMesh.createNode(storePath, { - documents: [ - bifrostDiscordCommandDefinition(), - bifrostDiscordReceiptDefinition(), - bifrostGenericDocumentDefinition("gamecult.eve.provider_advertisement", "gamecult.eve.provider_advertisement.v1", "providerId"), - bifrostGenericDocumentDefinition("gamecult.eve.surface_state", "gamecult.eve.surface_state.v1", "providerId"), - bifrostGenericDocumentDefinition("gamecult.eve.interface_binding", "gamecult.eve.interface_binding.v1", "bindingId"), - ], - }); -} - -function pumpBifrostCultMeshCommand(commandId: string): void { - const processor = resolve(config.bifrostRoot, "tools", "cultmesh-bridge-commands.mjs"); - const result = spawnSync(process.execPath, [ - processor, - "process", - "--store", - config.bifrostCultMesh.storePath, - "--command-id", - commandId, - ], { - cwd: config.bifrostRoot, - encoding: "utf8", - windowsHide: true, - }); - if (result.status !== 0 || result.error) { - throw new Error(`Bifrost CultMesh command processor failed. ${renderBridgeFailure(result, 1, 1)}`); - } -} - -async function waitForBifrostDiscordReceipt( - node: { get: (definition: unknown, key: string) => unknown; cache?: { pullAllBackingStores?: () => Promise } }, - commandId: string, - timeoutMs: number, -): Promise> { - const startedAt = Date.now(); - while (Date.now() - startedAt <= timeoutMs) { - await node.cache?.pullAllBackingStores?.(); - const receipt = unwrapBifrostRecord(await node.get(bifrostDiscordReceiptDefinition(), commandId)); - if (receipt) { - return receipt; - } - await delay(250); - } - throw new Error(`Timed out waiting for Bifrost CultMesh Discord receipt ${commandId}.`); -} - -let bifrostDefinitions: { - command?: unknown; - receipt?: unknown; - generic: Map; -} | undefined; - -function bifrostDiscordCommandDefinition(): unknown { - const { defineDocumentType } = loadBifrostCultMeshRuntime(); - bifrostDefinitions ??= { generic: new Map() }; - bifrostDefinitions.command ??= defineDocumentType({ - type: BIFROST_DISCORD_POST_COMMAND_DOCUMENT_TYPE, - schemaName: BIFROST_DISCORD_POST_COMMAND_DOCUMENT_TYPE, - schemaId: BIFROST_DISCORD_POST_COMMAND_SCHEMA_ID, - schemaVersion: BIFROST_DISCORD_POST_COMMAND_SCHEMA_ID, - contentHash: BIFROST_DISCORD_POST_COMMAND_SCHEMA_ID, - global: false, - name: "commandId", - schema: parseBifrostObjectDocument("Bifrost Discord post command"), - }); - return bifrostDefinitions.command; -} - -function bifrostDiscordReceiptDefinition(): unknown { - const { defineDocumentType } = loadBifrostCultMeshRuntime(); - bifrostDefinitions ??= { generic: new Map() }; - bifrostDefinitions.receipt ??= defineDocumentType({ - type: BIFROST_DISCORD_POST_RECEIPT_DOCUMENT_TYPE, - schemaName: BIFROST_DISCORD_POST_RECEIPT_DOCUMENT_TYPE, - schemaId: BIFROST_DISCORD_POST_RECEIPT_SCHEMA_ID, - schemaVersion: BIFROST_DISCORD_POST_RECEIPT_SCHEMA_ID, - contentHash: BIFROST_DISCORD_POST_RECEIPT_SCHEMA_ID, - global: false, - name: "commandId", - schema: parseBifrostObjectDocument("Bifrost Discord post receipt"), - }); - return bifrostDefinitions.receipt; -} - -function bifrostGenericDocumentDefinition(type: string, schemaId: string, name: string): unknown { - const { defineDocumentType } = loadBifrostCultMeshRuntime(); - bifrostDefinitions ??= { generic: new Map() }; - const key = `${type}:${schemaId}:${name}`; - let definition = bifrostDefinitions.generic.get(key); - if (!definition) { - definition = defineDocumentType({ - type, - schemaName: type, - schemaId, - schemaVersion: schemaId, - contentHash: schemaId, - global: false, - name, - schema: parseBifrostObjectDocument(type), - }); - bifrostDefinitions.generic.set(key, definition); - } - return definition; -} - -function loadBifrostCultMeshRuntime(): { - CultMesh: { createNode: (storePath: string, options: unknown) => Promise<{ - put: (definition: unknown, key: string, value: unknown) => Promise; - get: (definition: unknown, key: string) => unknown; - flush?: () => Promise; - cache?: { pullAllBackingStores?: () => Promise }; - }> }; - defineDocumentType: (definition: Record) => unknown; -} { - if (process.env.VOIDBOT_CULTLIB_ROOT) { - try { - const cultMeshPackage = resolve(process.env.VOIDBOT_CULTLIB_ROOT, "packages", "cultmesh-ts", "package.json"); - const cultCachePackage = resolve(process.env.VOIDBOT_CULTLIB_ROOT, "packages", "cultcache-ts", "package.json"); - const { CultMesh } = createRequire(cultMeshPackage)("./dist/index.js") as { - CultMesh?: { createNode: (storePath: string, options: unknown) => Promise<{ - put: (definition: unknown, key: string, value: unknown) => Promise; - get: (definition: unknown, key: string) => unknown; - flush?: () => Promise; - cache?: { pullAllBackingStores?: () => Promise }; - }> }; - }; - const { defineDocumentType } = createRequire(cultCachePackage)("./dist/index.js") as { - defineDocumentType?: (definition: Record) => unknown; - }; - if (CultMesh && defineDocumentType) { - return { CultMesh, defineDocumentType }; - } - } catch { - } - } - const candidates = [ - resolve(config.bifrostRoot, "..", "CultLib", "packages", "cultmesh-ts", "package.json"), - resolve(config.bifrostRoot, "..", "CultMeshTS", "package.json"), - ]; - - for (const packageJson of candidates) { - try { - const requireCult = createRequire(packageJson); - const { CultMesh } = requireCult("cultmesh-ts") as { - CultMesh?: { createNode: (storePath: string, options: unknown) => Promise<{ - put: (definition: unknown, key: string, value: unknown) => Promise; - get: (definition: unknown, key: string) => unknown; - flush?: () => Promise; - cache?: { pullAllBackingStores?: () => Promise }; - }> }; - }; - const { defineDocumentType } = requireCult("cultcache-ts") as { - defineDocumentType?: (definition: Record) => unknown; - }; - if (CultMesh && defineDocumentType) { - return { CultMesh, defineDocumentType }; - } - } catch { - } - } - throw new Error("CultMesh/CultCache packages are unavailable; cannot write Bifrost command documents."); -} - -function parseBifrostObjectDocument(label: string): { parse: (value: unknown) => unknown } { - return { - parse(value: unknown): unknown { - if (!value || typeof value !== "object") { - throw new Error(`${label} must be an object.`); - } - return value; - }, - }; -} - -function unwrapBifrostRecord(record: unknown): Record | undefined { - const candidate = Array.isArray(record) && record.length === 1 ? record[0] : record; - const value = isBifrostRecord(candidate) && isBifrostRecord(candidate.value) ? candidate.value : candidate; - return isBifrostRecord(value) ? value : undefined; -} - -function isBifrostRecord(value: unknown): value is Record { - return typeof value === "object" && value !== null; -} - -function bifrostString(value: unknown): string { - return typeof value === "string" ? value.trim() : value === undefined || value === null ? "" : String(value).trim(); -} - -function delay(ms: number): Promise { - return new Promise((resolveDelay) => setTimeout(resolveDelay, ms)); -} - async function resolveRepoIdentityForJobIntent( job: JobRecord, identitySelector?: string, diff --git a/notes/fresh-workspace-handoff.md b/notes/fresh-workspace-handoff.md index f69c552..52726d9 100644 --- a/notes/fresh-workspace-handoff.md +++ b/notes/fresh-workspace-handoff.md @@ -214,6 +214,7 @@ Git history and smoke artifacts carry routine proof. `state/evidence.jsonl` shou - Revision `21d12a3` adds the portable memory-maintenance organ behind that intent. It commits `void-memory-maintenance-attempt:` before its single bounded text-model call, validates the complete proposed operation batch, applies only the allowed memory/incubation/agency/candidate family, requires zero remaining short-term memory, and records `void-memory-maintenance:` only on success. Failure is visible and cannot retry until a different nap. Idunn/Ygg live promotion and receipt verification are the immediate deployment check. - The one-minute Persona scheduler pulse is an observation clock, not an inference cadence. With the swarm brake present it returns before registry, Discord, embedding, storage, or dispatch work; current Ygg logs show `selected: []` and `queued: []`. Unprompted speech also has a separate global admission window. Do not cool token use by making observation blind; keep turn authorization at the initiative owner. - Revision `f97e67b` adds the portable moderation-heartbeat coordinator and enforcement organ behind a separate disabled-by-default 15-minute cadence. The model returns an exact review ledger plus urgent IDs and typed case operations; missing evidence or unaccounted urgent evidence blocks cursor advancement. The attempt is receipted by latest message before inference, so one unchanged archive window cannot burn tokens repeatedly. Case enforcement is separate, scans typed pending debt on every cadence, and resolves a case only after a required Discord ban succeeds. Ygg's first `log_only` window at `0629fc7` failed safely with one 401, zero cases, zero cursor movement, and zero sanctions; both supported auth-header shapes prove the mounted MiMo credential itself is stale. `VOID_MODERATION_DAEMON_ENABLED` is false again pending a refreshed provider credential or deliberate alternate model port. +- The shared Bifrost Discord mouth now lives in `packages/core/src/bifrost-discord-command.ts`. Worker repo-Face speech and resident Void candidate delivery use the same stable-id typed command, provider-store write, optional Bifrost pump, and completed-receipt check. The candidate organ selects at most one targeted queued candidate and writes its speech receipt/`spoken` transition only after transport completes. Ygg mounts the Bifrost provider store and CultLib runtime into the scheduler; `VOID_CANDIDATE_DELIVERY_ENABLED` controls this transport-only organ independently from the disabled moderation/rumination model cadence. - Revisions `c0cf875` and `1527b29` extract person-shaped rumination and candidate delivery without scheduling them. Rumination context, operation policy, advocacy accountability, tool-free text actuation, and coordinator are portable; the attempt key hashes meaning-bearing pressure/evidence and ignores elapsed clock, so unchanged state cannot create periodic thought. Candidate delivery is deterministic and parent-owned: one targeted queued candidate, duplicate-receipt refusal, injected Bifrost port, then one `mark_candidate_intervention_spoken` operation with the transport receipt. The next cut is extracting the worker's existing typed Bifrost Discord command path into a shared port rather than duplicating CultMesh mechanics. - Deterministic state-packet assembly now lives in `persona-state-packet-projector.ts`, which takes typed state, registry/room evidence, pronoun guidance, curiosity facts, and an explicit observation time. The shell no longer owns section ordering or social-context composition. The downstream regex vocabulary cleaner was deleted: it was a symptom compensator rewriting ordinary words such as `witness` and `canary` after projection. A smoke proves literal source language survives the projector unchanged. - Optional model-backed state projection now crosses `persona-text-projection-actuator.ts`. That actuator alone owns Codex args/process lifetime, ordered fallback models, timeout, output decoding, retry classification, and model-output JSONL telemetry. Focused smoke proves it chooses the last visible agent message, retries capacity/429 failures, and refuses to classify prompt-contract failures as model availability problems. The heartbeat shell lost another 182 lines and now only supplies the completed projection prompt plus runtime config. diff --git a/notes/voidbot-current-system-map.md b/notes/voidbot-current-system-map.md index c796c4a..426b8a2 100644 --- a/notes/voidbot-current-system-map.md +++ b/notes/voidbot-current-system-map.md @@ -192,6 +192,7 @@ VoidBot runs on Yggdrasil. Idunn owns deployment and consumes upstream repositor - 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. +- `packages/core/src/bifrost-discord-command.ts` owns the typed Bifrost Discord command/receipt crossing for all VoidBot callers. Worker repo-Face speech and the resident `void-candidate-delivery-organ.ts` supply identity, target, reply, and content through that one port; neither owns CultMesh document definitions, command pumping, receipt polling, or transport completion policy. Candidate delivery marks typed state spoken only after a completed Bifrost receipt and is independently gated by `VOID_CANDIDATE_DELIVERY_ENABLED`. - `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/notes/voidbot-daemon-organ-migration-map.md b/notes/voidbot-daemon-organ-migration-map.md index 08eb312..2ef1cd1 100644 --- a/notes/voidbot-daemon-organ-migration-map.md +++ b/notes/voidbot-daemon-organ-migration-map.md @@ -21,6 +21,7 @@ The disabled tasks are not standby owners. The listed organs currently have no r | Persona initiative | VoidBot `persona-scheduler` | typed scheduler state, attention commands, Discord activity, Persona state, semantic pressure | approved Persona jobs and typed tick result | `repo-face-heartbeats.cc` plus attention inbox | `voidbot.service` / Compose | Windows scheduled tasks, per-pulse CLI children | | Void moderation heartbeat | VoidBot daemon moderation organ | server rules, chronological Discord evidence, infringement history, typed open cases | typed moderation operations and parent-owned enforcement requests/receipts | `void-self-state.cc` | `voidbot.service` / Compose | Persona initiative, prompt child, Windows task | | Void rumination | VoidBot daemon rumination organ | typed Mind, room evidence, repo activity, open obligations, speech receipts | typed Mind operations and candidate interventions; parent-owned delivery | `void-self-state.cc` | `voidbot.service` / Compose | moderation heartbeat, Persona scheduler, child model | +| Void candidate delivery | VoidBot daemon candidate-delivery organ through shared Bifrost command port | targeted queued candidate, canonical speech receipts, Bifrost typed command route | one typed Discord command and completed receipt; candidate marked spoken only afterward | `void-self-state.cc` and Bifrost provider store | `voidbot.service` / Compose | rumination model, worker-local transport clone, direct Discord script | | Void mood | VoidBot daemon physiology organ | typed affect/runtime documents, clock, active moderation witness | typed sleep and speaking-pressure operations; maintenance intent | `void-self-state.cc` | `voidbot.service` / Compose | memory maintenance, scheduler initiative, repair loops | | Void memory maintenance | VoidBot daemon memory organ | typed short/durable memory, incubation, candidates, sleep intent | validated memory/incubation/candidate operations | `void-self-state.cc` | `voidbot.service` / Compose | mood organ, rumination child, editable JSON projections | | Operations watchdog | Idunn/GameCult ops | systemd/Compose health, service endpoints, Postgres/Qdrant/Ollama, backup freshness | typed health witnesses, restart requests, operator notifications | Idunn/ops-owned health state; VoidBot may publish provider health | `idunn-yggdrasil.service` | VoidBot as sole self-health judge, Windows task | @@ -42,8 +43,8 @@ The disabled tasks are not standby owners. The listed organs currently have no r 1. Portable Void physiology is live in the daemon with injected typed state/moderation ports and maintenance intent; the Windows omnibus mood writer is deleted. 2. Portable memory maintenance is daemon-owned: physiology emits a typed nap intent; the memory organ owns bounded context projection, one text-model attempt per nap, whole-batch operation validation, typed application, and residue verification. The attempt receipt is committed before inference, so failure cannot become a periodic retry furnace. -3. The moderation-heartbeat half is portable and daemon-wired: chronological archive evidence, explicit model review ledger, typed case validation/application, guarded cursor commit, and replay-safe policy enforcement are separate organs. Live rollout begins in `log_only`; remove the old Windows heartbeat authority after the deployed review ledger and cases are inspected. Person-shaped rumination remains the next split from `run-void-moderator-rumination.ps1`. -4. Add serialized daemon cadence for those three Void-owned organs, with independent due state, timeout, and telemetry. Do not route them through Persona initiative. +3. Moderation heartbeat, enforcement, and person-shaped rumination are portable organs. Rumination may only emit typed operations and queued candidates; it cannot own transport. +4. Candidate delivery is a separate daemon organ and both it and worker Face speech use `postDiscordViaBifrostCultMesh`. Candidate delivery is enabled independently of model inference and closes state only after a completed typed Bifrost receipt. 5. Give Bifrost dispatch to a Bifrost-owned resident worker. 6. Give operations health/restart scheduling to Idunn and retain VoidBot health publication as provider-owned observation only. diff --git a/packages/config/src/index.ts b/packages/config/src/index.ts index 06fb887..31cf4aa 100644 --- a/packages/config/src/index.ts +++ b/packages/config/src/index.ts @@ -140,6 +140,9 @@ const envSchema = z.object({ VOID_MODERATION_DAEMON_ENABLED: booleanFromEnv.default(false), VOID_MODERATION_DAEMON_INTERVAL_MINUTES: z.coerce.number().int().min(5).default(15), VOID_MODERATION_ENFORCEMENT_MODE: z.string().trim().min(1).default("log_only"), + VOID_CANDIDATE_DELIVERY_ENABLED: booleanFromEnv.default(false), + VOID_PUBLIC_ROOM_PERSONA_NAME: z.string().trim().min(1).default("Void"), + VOID_PUBLIC_ROOM_PERSONA_AVATAR_URL: optionalNonEmptyString, REPO_FACE_HEARTBEAT_STATE_PATH: z.string().min(1).default(".voidbot/status/repo-face-heartbeats.cc"), REPO_FACE_HEARTBEAT_TASK_NAME: z.string().min(1).default("VoidBot Repo Face Heartbeats"), REPO_FACE_HEARTBEAT_INTERVAL_MINUTES: z.coerce.number().int().min(1).default(5), @@ -252,6 +255,7 @@ export interface AppConfig { }; voidModerationHeartbeatEnabled: boolean; voidModerationDaemon: { enabled: boolean; intervalMinutes: number; enforcementMode: string }; + voidCandidateDelivery: { enabled: boolean; personaName: string; personaAvatarUrl?: string }; repoFaceHeartbeats: { enabled: boolean; statePath: string; @@ -547,6 +551,11 @@ export function loadConfig(env: NodeJS.ProcessEnv = process.env): AppConfig { intervalMinutes: parsed.VOID_MODERATION_DAEMON_INTERVAL_MINUTES, enforcementMode: parsed.VOID_MODERATION_ENFORCEMENT_MODE, }, + voidCandidateDelivery: { + enabled: parsed.VOID_CANDIDATE_DELIVERY_ENABLED, + personaName: parsed.VOID_PUBLIC_ROOM_PERSONA_NAME, + personaAvatarUrl: parsed.VOID_PUBLIC_ROOM_PERSONA_AVATAR_URL, + }, repoFaceHeartbeats: { enabled: parsed.REPO_FACE_HEARTBEATS_ENABLED, statePath: canonicalSchedulerStatePath(resolve(parsed.REPO_FACE_HEARTBEAT_STATE_PATH)), diff --git a/packages/core/src/bifrost-discord-command.ts b/packages/core/src/bifrost-discord-command.ts new file mode 100644 index 0000000..22162be --- /dev/null +++ b/packages/core/src/bifrost-discord-command.ts @@ -0,0 +1,120 @@ +import { createHash } from "node:crypto"; +import { spawnSync } from "node:child_process"; +import { createRequire } from "node:module"; +import { resolve } from "node:path"; + +const COMMAND_TYPE = "bifrost.bridge.discord_post_command"; +const COMMAND_SCHEMA = "bifrost.bridge.discord_post_command.v1"; +const RECEIPT_TYPE = "bifrost.bridge.discord_post_receipt"; +const RECEIPT_SCHEMA = "bifrost.bridge.discord_post_receipt.v1"; + +export interface BifrostDiscordCommandConfig { + commandUri: string; + storePath: string; + timeoutMs: number; + pumpEnabled: boolean; + bifrostRoot: string; + cultlibRoot?: string; +} + +export interface BifrostDiscordPostInput { + idempotencyKey: string; + source: { kind: string; id: string; jobId?: string; requestMessageId?: string }; + actor: { id: string; displayName: string; repoName?: string }; + channelId: string; + content: string; + replyToMessageId?: string; + personaAvatarUrl?: string; +} + +export interface BifrostDiscordPostReceipt { commandId: string; messageId: string; transport: string; url?: string } + +interface CommandNode { + put: (definition: unknown, key: string, value: unknown) => Promise; + get: (definition: unknown, key: string) => unknown; + flush?: () => Promise; + cache?: { pullAllBackingStores?: () => Promise }; +} + +export async function postDiscordViaBifrostCultMesh(input: BifrostDiscordPostInput, config: BifrostDiscordCommandConfig, dependencies: { + openNode?: () => Promise; + definitions?: { command: unknown; receipt: unknown; generic: unknown[] }; + pump?: (commandId: string) => void; + now?: () => Date; + pause?: (ms: number) => Promise; +} = {}): Promise { + if (!/^cultmesh:\/\/[^/]+\/commands\/discord-post(?:$|[/?#])/.test(config.commandUri)) throw new Error(`BIFROST_CULTMESH_COMMAND_URI must be a CultMesh Discord command URI, got "${config.commandUri}".`); + if (!input.channelId || !input.content.trim()) throw new Error("Bifrost Discord post requires channel and content."); + const commandId = `voidbot-discord-${createHash("sha1").update(input.idempotencyKey).digest("hex").slice(0, 20)}`; + const definitions = dependencies.definitions ?? loadDefinitions(config); + const node = await (dependencies.openNode ?? (() => openNode(config, definitions)))(); + const requestedAt = (dependencies.now?.() ?? new Date()).toISOString(); + await node.put(definitions.command, commandId, { + schemaName: COMMAND_TYPE, schemaVersion: COMMAND_SCHEMA, commandId, command: "discord-post", status: "pending", + requestedBy: "voidbot", requestedAt, updatedAt: requestedAt, commandUri: config.commandUri, + source: input.source, actor: input.actor, + payload: { + identityId: input.actor.id, channelId: input.channelId, content: input.content, + personaName: input.actor.displayName, personaAvatarUrl: input.personaAvatarUrl ?? "", replyToMessageId: input.replyToMessageId ?? "", + }, + }); + await node.flush?.(); + if (config.pumpEnabled) (dependencies.pump ?? ((id) => pumpCommand(id, config)))(commandId); + const receipt = await waitForReceipt(node, definitions.receipt, commandId, config.timeoutMs, dependencies.pause ?? delay); + if (receipt.status !== "completed" || receipt.ok !== true) throw new Error(`Bifrost CultMesh Discord command ${commandId} failed: ${stringValue(receipt.error) || "no error detail"}`); + const messageId = stringValue(receipt.messageId); + const transport = stringValue(receipt.transport); + if (!messageId || !transport) throw new Error(`Bifrost CultMesh Discord command ${commandId} returned an incomplete receipt.`); + return { commandId, messageId, transport, url: stringValue(receipt.url) }; +} + +async function waitForReceipt(node: CommandNode, definition: unknown, commandId: string, timeoutMs: number, pause: (ms: number) => Promise): Promise> { + const startedAt = Date.now(); + while (Date.now() - startedAt <= timeoutMs) { + await node.cache?.pullAllBackingStores?.(); + const receipt = unwrapRecord(await node.get(definition, commandId)); + if (receipt) return receipt; + await pause(250); + } + throw new Error(`Timed out waiting for Bifrost CultMesh Discord receipt ${commandId}.`); +} + +function openNode(config: BifrostDiscordCommandConfig, definitions: { command: unknown; receipt: unknown; generic: unknown[] }): Promise { + const { CultMesh } = loadRuntime(config); + return CultMesh.createNode(config.storePath, { documents: [definitions.command, definitions.receipt, ...definitions.generic] }); +} + +function loadDefinitions(config: BifrostDiscordCommandConfig): { command: unknown; receipt: unknown; generic: unknown[] } { + const { defineDocumentType } = loadRuntime(config); + const define = (type: string, schemaId: string, name: string) => defineDocumentType({ type, schemaName: type, schemaId, schemaVersion: schemaId, contentHash: schemaId, global: false, name, schema: objectParser(type) }); + return { + command: define(COMMAND_TYPE, COMMAND_SCHEMA, "commandId"), receipt: define(RECEIPT_TYPE, RECEIPT_SCHEMA, "commandId"), + generic: [define("gamecult.eve.provider_advertisement", "gamecult.eve.provider_advertisement.v1", "providerId"), define("gamecult.eve.surface_state", "gamecult.eve.surface_state.v1", "providerId"), define("gamecult.eve.interface_binding", "gamecult.eve.interface_binding.v1", "bindingId")], + }; +} + +function loadRuntime(config: BifrostDiscordCommandConfig): { CultMesh: { createNode: (path: string, options: unknown) => Promise }; defineDocumentType: (definition: Record) => unknown } { + const candidates = config.cultlibRoot ? [ + [resolve(config.cultlibRoot, "packages", "cultmesh-ts", "package.json"), "./dist/index.js", resolve(config.cultlibRoot, "packages", "cultcache-ts", "package.json"), "./dist/index.js"], + ] : []; + candidates.push([resolve(config.bifrostRoot, "..", "CultLib", "packages", "cultmesh-ts", "package.json"), "cultmesh-ts", resolve(config.bifrostRoot, "..", "CultLib", "packages", "cultcache-ts", "package.json"), "cultcache-ts"]); + for (const [meshPackage, meshModule, cachePackage, cacheModule] of candidates) { + try { + const { CultMesh } = createRequire(meshPackage)(meshModule) as { CultMesh?: { createNode: (path: string, options: unknown) => Promise } }; + const { defineDocumentType } = createRequire(cachePackage)(cacheModule) as { defineDocumentType?: (definition: Record) => unknown }; + if (CultMesh && defineDocumentType) return { CultMesh, defineDocumentType }; + } catch { } + } + throw new Error("CultMesh/CultCache packages are unavailable; cannot write Bifrost command documents."); +} + +function pumpCommand(commandId: string, config: BifrostDiscordCommandConfig): void { + const result = spawnSync(process.execPath, [resolve(config.bifrostRoot, "tools", "cultmesh-bridge-commands.mjs"), "process", "--store", config.storePath, "--command-id", commandId], { cwd: config.bifrostRoot, encoding: "utf8", windowsHide: true }); + if (result.status !== 0 || result.error) throw new Error(`Bifrost CultMesh command processor failed: ${result.stderr || result.stdout || result.error?.message || "unknown failure"}`); +} + +function objectParser(label: string): { parse: (value: unknown) => unknown } { return { parse(value) { if (!value || typeof value !== "object") throw new Error(`${label} must be an object.`); return value; } }; } +function unwrapRecord(record: unknown): Record | undefined { const candidate = Array.isArray(record) && record.length === 1 ? record[0] : record; const value = isRecord(candidate) && isRecord(candidate.value) ? candidate.value : candidate; return isRecord(value) ? value : undefined; } +function isRecord(value: unknown): value is Record { return typeof value === "object" && value !== null; } +function stringValue(value: unknown): string | undefined { return typeof value === "string" && value.trim() ? value.trim() : undefined; } +function delay(ms: number): Promise { return new Promise((resolveDelay) => setTimeout(resolveDelay, ms)); } diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 7243c7d..7b5c46e 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -26,3 +26,4 @@ export * from "./void-self-state-projection"; export * from "./void-self-state-service"; export * from "./persona-state-domain"; export * from "./persona-state-service"; +export * from "./bifrost-discord-command"; diff --git a/scripts/smoke-persona-initiative-engine.ts b/scripts/smoke-persona-initiative-engine.ts index d5d8d4a..b7de063 100644 --- a/scripts/smoke-persona-initiative-engine.ts +++ b/scripts/smoke-persona-initiative-engine.ts @@ -5,6 +5,7 @@ import { join } from "node:path"; import { queueAgentHeartbeatMention, + postDiscordViaBifrostCultMesh, ensureVoidSelfStateIdentityProfile, createEmptyVoidSelfState, readRepoFaceMentionInbox, @@ -999,6 +1000,37 @@ assert.equal(repeatedRumination.status, "ok", "a meaning-bearing state change ma const settledRumination = await runVoidRumination({ statePath: "void-self-state.cc", observedAt: new Date("2026-07-16T05:00:00.000Z"), pendingMentions: [{ messageId: "message-1", channelId: "room-1" }], recentHistory: { messages: [{ id: "message-1", timestamp: "2026-07-16T02:55:00.000Z" }] }, doctrine: "Participate honestly.", rules: "Fixture rules.", voice: "Dry and direct." }, ruminationDependencies as never); assert.equal(settledRumination.status, "skipped", "an unchanged settled pressure cannot become a timer-driven inference loop"); assert.equal(ruminationModelCalls, 2, "rumination cadence follows changed pressure rather than elapsed clock"); +const bifrostWrites: Array<{ key: string; value: Record }> = []; +const bifrostPostInput = { + idempotencyKey: "candidate-1:room-1:message-1", + source: { kind: "voidbot.void.candidate", id: "candidate-1" }, + actor: { id: "void", displayName: "Void" }, + channelId: "room-1", + replyToMessageId: "message-1", + content: "This answer stays with the conversation that made it necessary.", +}; +const postThroughFakeBifrost = () => postDiscordViaBifrostCultMesh(bifrostPostInput, { + commandUri: "cultmesh://asgard.yggdrasil.bifrost/commands/discord-post", + storePath: "provider-store.cc", + timeoutMs: 100, + pumpEnabled: false, + bifrostRoot: "Bifrost", +}, { + definitions: { command: "command-definition", receipt: "receipt-definition", generic: [] }, + openNode: async () => ({ + put: async (_definition, key, value) => { bifrostWrites.push({ key, value: value as Record }); }, + get: (_definition, key) => ({ status: "completed", ok: true, commandId: key, messageId: "posted-message-1", transport: "webhook" }), + }), + now: () => physiologyObservedAt, +}); +const firstBifrostReceipt = await postThroughFakeBifrost(); +const secondBifrostReceipt = await postThroughFakeBifrost(); +assert.equal(firstBifrostReceipt.commandId, secondBifrostReceipt.commandId, "the shared Bifrost port derives a stable command id from its caller-owned idempotency key"); +assert.deepEqual(firstBifrostReceipt, { commandId: firstBifrostReceipt.commandId, messageId: "posted-message-1", transport: "webhook", url: undefined }, "the shared port returns only a completed typed transport receipt"); +assert.deepEqual((bifrostWrites[0].value.payload as Record), { + identityId: "void", channelId: "room-1", content: bifrostPostInput.content, + personaName: "Void", personaAvatarUrl: "", replyToMessageId: "message-1", +}, "the shared port preserves actor, room, reply, and content in the typed command payload"); let deliveredCandidate: { channelId?: string; replyToMessageId?: string; content?: string } = {}; const candidateDelivery = await runVoidCandidateDelivery({ statePath: "void-self-state.cc", personaName: "Void", observedAt: physiologyObservedAt }, { loadState: async () => ruminationState, diff --git a/state/evidence.jsonl b/state/evidence.jsonl index a3fc488..4854d73 100644 --- a/state/evidence.jsonl +++ b/state/evidence.jsonl @@ -447,3 +447,4 @@ {"ts":"2026-07-16T04:15:05Z","type":"implementation","status":"ok","note":"Portable moderation heartbeat at f97e67b separates evidence acquisition, exact review ledger and typed case application, guarded cursor commit, bounded per-window inference, and replay-safe Discord enforcement. Deployed fixture passed on Ygg; live rollout is log_only before sanctions."} {"ts":"2026-07-16T04:21:02Z","type":"verification","status":"blocked","note":"Ygg live moderation rollout at 0629fc7 failed safely with one pre-receipted 401, zero cases, zero cursor movement, and zero sanctions. Both supported auth-header forms prove the mounted MiMo credential is stale. Cadence returned to disabled before the next window; deployed fixtures remain green."} {"ts":"2026-07-16T04:30:45Z","type":"implementation","status":"ok","note":"Person-shaped rumination extracted at c0cf875 with stable pressure fingerprints, typed awake-operation policy, advocacy accountability, and no transport authority. Candidate delivery at 1527b29 deterministically lowers at most one targeted candidate through an injected Bifrost port and records a typed spoken receipt."} +{"ts":"2026-07-16T04:42:32Z","type":"implementation","status":"ok","note":"Worker repo-Face speech and resident Void candidate delivery now share one typed Bifrost CultMesh Discord command/receipt port. Candidate delivery is transport-only, independently gated, and marks candidates spoken only after a complete receipt; Ygg live inspection found zero queued candidates before enablement."} diff --git a/state/map.yaml b/state/map.yaml index f73cd90..88775be 100644 --- a/state/map.yaml +++ b/state/map.yaml @@ -290,5 +290,5 @@ rejected_paths: reason: sleep/distillation that collapses concrete claims into generic abstraction creates memory nonsense and then forces later cleanup compensators 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. Build the portable memory-maintenance organ from `run-void-memory-maintenance.ps1`: separate typed context acquisition/projection, model actuation, operation parsing/validation, and typed application; consume physiology maintenance intent with nap-level dedupe. Preserve the existing memory lifecycle invariants and fixture evidence. Do not revive PowerShell as daemon runtime and do not unpause without an explicit controlled-live-fire decision. + summary: VoidBot's resident Yggdrasil daemon owns Persona initiative, physiology, nap-deduped memory maintenance, moderation heartbeat/enforcement, and bounded person-shaped rumination as separate organs. The swarm is paused, moderation and rumination model cadences remain cold while the MiMo credential is stale, and semantic pressure can authorize at most one committed turn per evidence item. Worker Face speech and Void candidate delivery now share one typed Bifrost CultMesh Discord command/receipt port; candidate delivery remains a separate parent-owned organ and never lets the rumination model touch transport. + next_action: Deploy and verify the shared Bifrost port and candidate-delivery organ on Yggdrasil through Idunn. Confirm the scheduler sees the shared Bifrost provider store and CultLib runtime, candidate delivery reports an empty queue without transport, the swarm stays paused, and no model cadence wakes. Then continue deleting superseded Windows moderation/rumination authority only after the equivalent daemon paths have live evidence.