Move moderation heartbeat into daemon organs

This commit is contained in:
Meta Vens 2026-07-16 05:10:23 +01:00
commit f97e67b1dd
10 changed files with 370 additions and 46 deletions

View file

@ -89,6 +89,9 @@ METAME_OWNER_VOICE_SOURCE_CHANNEL_ID=
METAME_OWNER_VOICE_PERSONA_ID=metame
METAME_OWNER_VOICE_STATE_PATH=.voidbot/private/personas/metame/metame-owner-voice-sync.md
REPO_FACE_HEARTBEATS_ENABLED=false
VOID_MODERATION_DAEMON_ENABLED=false
VOID_MODERATION_DAEMON_INTERVAL_MINUTES=15
VOID_MODERATION_ENFORCEMENT_MODE=log_only
REPO_FACE_HEARTBEAT_STATE_PATH=.voidbot/status/repo-face-heartbeats.json
REPO_FACE_HEARTBEAT_TASK_NAME=VoidBot Repo Face Heartbeats
REPO_FACE_HEARTBEAT_INTERVAL_MINUTES=15

View file

@ -0,0 +1,40 @@
import { readFile } from "node:fs/promises";
import type { loadConfig } from "@voidbot/config";
export async function requestBoundedTextCompletion(input: {
prompt: string;
config: ReturnType<typeof loadConfig>;
purpose: string;
model?: string;
maxCompletionTokens: number;
}, dependencies: { fetch?: typeof fetch; readText?: (path: string) => Promise<string> } = {}): Promise<string> {
const api = input.config.openAiApi;
const apiKey = api.apiKey ?? (api.apiKeyFile ? (await (dependencies.readText ?? ((path) => readFile(path, "utf8")))(api.apiKeyFile)).trim() : undefined);
if (!apiKey) throw new Error(`${input.purpose} requires the configured OpenAI-compatible API key.`);
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), api.timeoutMs);
try {
const response = await (dependencies.fetch ?? fetch)(`${api.baseUrl.replace(/\/+$/, "")}/chat/completions`, {
method: "POST",
headers: { "content-type": "application/json", [api.authHeader]: api.authHeader.toLowerCase() === "authorization" ? `Bearer ${apiKey}` : apiKey },
body: JSON.stringify({
model: input.model ?? api.model,
messages: [{ role: "user", content: input.prompt }],
stream: false,
max_completion_tokens: Math.min(api.maxCompletionTokens, Math.max(256, input.maxCompletionTokens)),
}),
signal: controller.signal,
});
const responseText = await response.text();
if (!response.ok) throw new Error(`${input.purpose} request failed with ${response.status}: ${redact(responseText, apiKey).slice(0, 1000)}`);
const payload = JSON.parse(responseText) as { choices?: Array<{ message?: { content?: unknown } }> };
const content = payload.choices?.[0]?.message?.content;
if (typeof content !== "string" || !content.trim()) throw new Error(`${input.purpose} response contained no assistant text.`);
return content.trim();
} catch (error) {
if (error instanceof Error && error.name === "AbortError") throw new Error(`${input.purpose} request timed out after ${api.timeoutMs}ms.`);
throw error;
} finally { clearTimeout(timeout); }
}
function redact(value: string, secret: string): string { return secret ? value.split(secret).join("[redacted]") : value; }

View file

@ -1,11 +1,15 @@
import "dotenv/config";
import { loadConfig } from "@voidbot/config";
import { readFile } from "node:fs/promises";
import { resolve } from "node:path";
import { runPersonaSchedulerTick } from "./persona-scheduler-runner.js";
import { runVoidPhysiologyOrgan } from "./void-physiology-organ.js";
import { runVoidMemoryMaintenance } from "./void-memory-maintenance-organ.js";
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";
const config = loadConfig();
const intervalMs = Math.max(1, config.repoFaceHeartbeats.intervalMinutes) * 60_000;
@ -13,7 +17,9 @@ let stopping = false;
let activeTick: Promise<void> | undefined;
let timer: NodeJS.Timeout | undefined;
let lastPhysiologyAt = 0;
let lastModerationAt = 0;
const physiologyIntervalMs = Math.max(5, Number(process.env.VOIDBOT_MOOD_INTERVAL_MINUTES) || 5) * 60_000;
const moderationIntervalMs = config.voidModerationDaemon.intervalMinutes * 60_000;
function runTick(): Promise<void> {
if (activeTick) {
@ -43,6 +49,30 @@ function runTick(): Promise<void> {
console.error(`Void physiology pulse failed. ${error instanceof Error ? error.stack ?? error.message : String(error)}`);
}
}
if (config.voidModerationDaemon.enabled && Date.now() - lastModerationAt >= moderationIntervalMs) {
lastModerationAt = Date.now();
try {
const moderation = await runVoidModerationHeartbeat({
statePath: resolve(config.storageRoot, "private", "void-self-state.cc"),
rules: await readFile(resolve("config", "discord-server-rules.md"), "utf8"),
enforcementMode: config.voidModerationDaemon.enforcementMode,
}, { projectText: (prompt) => projectVoidModerationOperations({ prompt, config }) });
console.log(`Void moderation-heartbeat pulse completed. ${JSON.stringify(moderation)}`);
} catch (error) {
console.error(`Void moderation-heartbeat pulse failed without automatic evidence-window retry. ${error instanceof Error ? error.stack ?? error.message : String(error)}`);
}
try {
const enforcement = await runVoidModerationEnforcement({
statePath: resolve(config.storageRoot, "private", "void-self-state.cc"),
mode: config.voidModerationDaemon.enforcementMode,
botToken: config.botToken,
guildId: config.developmentGuildId,
});
console.log(`Void moderation-enforcement pulse completed. ${JSON.stringify(enforcement)}`);
} catch (error) {
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)}`);
}
}
})
.catch((error) => { console.error(`Persona scheduler tick failed (started ${startedAt}). ${error instanceof Error ? error.stack ?? error.message : String(error)}`); })
.finally(() => { activeTick = undefined; });

View file

@ -1,33 +1,16 @@
import { readFile } from "node:fs/promises";
import type { loadConfig } from "@voidbot/config";
import { requestBoundedTextCompletion } from "./bounded-text-completion-actuator.js";
export async function projectVoidMemoryOperations(input: {
prompt: string;
config: ReturnType<typeof loadConfig>;
model?: string;
}, dependencies: { fetch?: typeof fetch; readText?: (path: string) => Promise<string> } = {}): Promise<string> {
const api = input.config.openAiApi;
const apiKey = api.apiKey ?? (api.apiKeyFile ? (await (dependencies.readText ?? ((path) => readFile(path, "utf8")))(api.apiKeyFile)).trim() : undefined);
if (!apiKey) throw new Error("Void memory maintenance requires the configured OpenAI-compatible API key.");
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), api.timeoutMs);
try {
const response = await (dependencies.fetch ?? fetch)(`${api.baseUrl.replace(/\/+$/, "")}/chat/completions`, {
method: "POST",
headers: { "content-type": "application/json", [api.authHeader]: api.authHeader.toLowerCase() === "authorization" ? `Bearer ${apiKey}` : apiKey },
body: JSON.stringify({ model: input.model ?? process.env.VOID_MEMORY_MAINTENANCE_MODEL ?? api.model, messages: [{ role: "user", content: input.prompt }], stream: false, max_completion_tokens: Math.min(api.maxCompletionTokens, Math.max(256, Number(process.env.VOID_MEMORY_MAINTENANCE_MAX_COMPLETION_TOKENS) || 2048)) }),
signal: controller.signal,
});
const responseText = await response.text();
if (!response.ok) throw new Error(`Void memory model request failed with ${response.status}: ${redact(responseText, apiKey).slice(0, 1000)}`);
const payload = JSON.parse(responseText) as { choices?: Array<{ message?: { content?: unknown } }> };
const content = payload.choices?.[0]?.message?.content;
if (typeof content !== "string" || !content.trim()) throw new Error("Void memory model response contained no assistant text.");
return content.trim();
} catch (error) {
if (error instanceof Error && error.name === "AbortError") throw new Error(`Void memory model request timed out after ${api.timeoutMs}ms.`);
throw error;
} finally { clearTimeout(timeout); }
return requestBoundedTextCompletion({
prompt: input.prompt,
config: input.config,
purpose: "Void memory maintenance",
model: input.model ?? process.env.VOID_MEMORY_MAINTENANCE_MODEL,
maxCompletionTokens: Number(process.env.VOID_MEMORY_MAINTENANCE_MAX_COMPLETION_TOKENS) || 2048,
}, dependencies);
}
function redact(value: string, secret: string): string { return secret ? value.split(secret).join("[redacted]") : value; }

View file

@ -0,0 +1,86 @@
import { applyVoidSelfStateOperation, loadVoidSelfStateTypedDocuments } from "@voidbot/core";
const STRIKE_EXPIRY_DAYS: Record<string, number> = {
safety_threat: 365, weaponized_intimidation: 365, stalking_or_doxxing: 365,
sexual_boundary_violation: 365, bigotry_identity_attack: 180, bad_faith_argument: 90,
nsfw_channel_violation: 30, spam_or_deceptive_promotion: 30, moderator_obstruction: 90,
empty_words_noise: 14, values_debate_escalation: 14, pg13_language_violation: 14,
event_time_coordination: 14,
};
export interface VoidModerationEnforcementResult {
status: "ok";
mode: string;
evaluatedCaseCount: number;
actions: Array<{ sourceMessageId: string; status: string; authorId?: string; infringementType?: string; strikeCount?: number }>;
}
export async function runVoidModerationEnforcement(input: {
statePath: string;
mode: string;
botToken?: string;
guildId?: string;
observedAt?: Date;
}, dependencies: {
loadState?: typeof loadVoidSelfStateTypedDocuments;
applyOperation?: typeof applyVoidSelfStateOperation;
banMember?: (input: { botToken: string; guildId: string; userId: string; reason: string }) => Promise<void>;
} = {}): Promise<VoidModerationEnforcementResult> {
const mode = input.mode.trim().toLowerCase();
const state = await (dependencies.loadState ?? loadVoidSelfStateTypedDocuments)({ canonicalPath: input.statePath });
const pending = state.moderationCursor.openCases.filter((entry) => entry.status === "pending" || entry.status === "watching");
const actions: VoidModerationEnforcementResult["actions"] = [];
for (const moderationCase of pending) {
const tags = new Set(moderationCase.tags.map((tag) => tag.toLowerCase()));
const infringementType = [...tags].find((tag) => tag.startsWith("infringement:"))?.slice("infringement:".length);
if (!infringementType || STRIKE_EXPIRY_DAYS[infringementType] === undefined || !moderationCase.authorId) {
actions.push({ sourceMessageId: moderationCase.sourceMessageId, status: "missing_policy_or_author", authorId: moderationCase.authorId, infringementType });
continue;
}
if (tags.has("moderation:case_only")) {
actions.push({ sourceMessageId: moderationCase.sourceMessageId, status: "case_recorded_no_sanction", authorId: moderationCase.authorId, infringementType });
continue;
}
if (!["policy", "enforce_policy", "enforce-policy", "ban"].includes(mode)) {
actions.push({ sourceMessageId: moderationCase.sourceMessageId, status: "enforcement_mode_non_destructive", authorId: moderationCase.authorId, infringementType });
continue;
}
const strikeCount = countActiveStrikes(state.moderationCursor.openCases, moderationCase.authorId, infringementType, STRIKE_EXPIRY_DAYS[infringementType], input.observedAt ?? new Date());
const instantBan = tags.has("moderation:instaban") || tags.has("severity:instaban");
const thirdStrike = tags.has("moderation:strike") && strikeCount >= 3;
if (instantBan || thirdStrike) {
if (!input.botToken || !input.guildId) throw new Error("Policy moderation requires Discord bot token and guild id.");
const reason = instantBan ? `VoidBot instaban: ${infringementType} at ${moderationCase.sourceMessageId}` : `VoidBot three-strike ban: ${infringementType} strike ${strikeCount}/3 at ${moderationCase.sourceMessageId}`;
await (dependencies.banMember ?? banDiscordMember)({ botToken: input.botToken, guildId: input.guildId, userId: moderationCase.authorId, reason });
await closeCase(input.statePath, moderationCase.sourceMessageId, input.observedAt ?? new Date(), `${instantBan ? "Instaban" : "Three-strike ban"} applied for ${infringementType}.`, dependencies.applyOperation);
actions.push({ sourceMessageId: moderationCase.sourceMessageId, status: instantBan ? "instaban_applied" : "three_strike_ban_applied", authorId: moderationCase.authorId, infringementType, strikeCount });
continue;
}
if (tags.has("moderation:strike")) {
await closeCase(input.statePath, moderationCase.sourceMessageId, input.observedAt ?? new Date(), `Strike ${strikeCount}/3 recorded for ${infringementType}; expires after ${STRIKE_EXPIRY_DAYS[infringementType]} days without another matching strike.`, dependencies.applyOperation);
actions.push({ sourceMessageId: moderationCase.sourceMessageId, status: "strike_recorded", authorId: moderationCase.authorId, infringementType, strikeCount });
}
}
return { status: "ok", mode, evaluatedCaseCount: pending.length, actions };
}
function countActiveStrikes(cases: Array<{ authorId?: string; createdAt: string; tags: string[] }>, authorId: string, infringementType: string, expiresDays: number, now: Date): number {
const cutoff = now.getTime() - expiresDays * 86_400_000;
return cases.filter((entry) => entry.authorId === authorId
&& entry.tags.map((tag) => tag.toLowerCase()).includes(`infringement:${infringementType}`)
&& entry.tags.map((tag) => tag.toLowerCase()).includes("moderation:strike")
&& Date.parse(entry.createdAt) >= cutoff).length;
}
async function closeCase(statePath: string, sourceMessageId: string, observedAt: Date, resolutionSummary: string, applyOperation = applyVoidSelfStateOperation): Promise<void> {
await applyOperation({ canonicalPath: statePath }, { operation: "close_open_case", sourceMessageId, status: "resolved", resolvedAt: observedAt.toISOString(), resolutionSummary });
}
async function banDiscordMember(input: { botToken: string; guildId: string; userId: string; reason: string }): Promise<void> {
const response = await fetch(`https://discord.com/api/v10/guilds/${input.guildId}/bans/${input.userId}`, {
method: "PUT",
headers: { Authorization: `Bot ${input.botToken}`, "Content-Type": "application/json", "X-Audit-Log-Reason": encodeURIComponent(input.reason) },
body: JSON.stringify({ delete_message_seconds: 0 }),
});
if (!response.ok) throw new Error(`Discord ban failed with ${response.status}: ${(await response.text()).slice(0, 1000)}`);
}

View file

@ -0,0 +1,62 @@
import { applyVoidSelfStateOperation, loadVoidSelfStateTypedDocuments, type VoidSelfStateOperation } from "@voidbot/core";
import { readVoidModerationEvidence, type VoidModerationEvidence } from "./void-moderation-evidence-source.js";
import { buildVoidModerationHeartbeatPrompt, parseVoidModerationHeartbeatOperations, projectVoidModerationHeartbeatContext } from "./void-moderation-heartbeat-projector.js";
export type VoidModerationHeartbeatResult =
| { status: "skipped"; reason: "no_new_messages" | "already_attempted_this_window"; runnerId?: string }
| { status: "ok"; runnerId: string; reviewedMessageCount: number; urgentMessageCount: number; appliedOperationCount: number; enforcementOperations: VoidSelfStateOperation[] };
export async function runVoidModerationHeartbeat(input: {
statePath: string;
rules: string;
enforcementMode: string;
observedAt?: Date;
}, dependencies: {
loadState?: typeof loadVoidSelfStateTypedDocuments;
applyOperation?: typeof applyVoidSelfStateOperation;
readEvidence?: (input: { priorCursorTimestamp?: string }) => Promise<VoidModerationEvidence>;
projectText: (prompt: string) => Promise<string>;
}): Promise<VoidModerationHeartbeatResult> {
const loadState = dependencies.loadState ?? loadVoidSelfStateTypedDocuments;
const applyOperation = dependencies.applyOperation ?? applyVoidSelfStateOperation;
const state = await loadState({ canonicalPath: input.statePath });
const evidence = await (dependencies.readEvidence ?? ((request) => readVoidModerationEvidence(request)))({
priorCursorTimestamp: state.moderationCursor.lastReviewedTimestamp,
});
if (evidence.messages.length === 0 || !evidence.observedCursor) return { status: "skipped", reason: "no_new_messages" };
const runnerId = `void-moderation-heartbeat-attempt:${evidence.observedCursor.lastReviewedMessageId}`;
if (state.scheduledRuntime.lastRuns.some((run) => run.runner === runnerId)) return { status: "skipped", reason: "already_attempted_this_window", runnerId };
const observedAt = input.observedAt ?? new Date();
await applyOperation({ canonicalPath: input.statePath }, {
operation: "record_scheduled_run",
run: { runner: runnerId, ranAt: observedAt.toISOString(), summary: `Started one bounded moderation review through message ${evidence.observedCursor.lastReviewedMessageId}.` },
});
const context = projectVoidModerationHeartbeatContext({
state,
observedAt,
observedCursor: evidence.observedCursor,
recentHistory: evidence,
urgentModerationWitnesses: [],
enforcementMode: input.enforcementMode,
rules: input.rules,
});
const decision = parseVoidModerationHeartbeatOperations({
outputText: await dependencies.projectText(buildVoidModerationHeartbeatPrompt(context)),
state,
observedMessageIds: evidence.messages.map((message) => message.id),
});
for (const operation of decision.operations) await applyOperation({ canonicalPath: input.statePath }, operation);
await applyOperation({ canonicalPath: input.statePath }, {
operation: "record_reviewed_messages",
lastReviewedMessageId: evidence.observedCursor.lastReviewedMessageId,
lastReviewedTimestamp: evidence.observedCursor.lastReviewedTimestamp,
});
return {
status: "ok",
runnerId,
reviewedMessageCount: decision.reviewedMessageIds.length,
urgentMessageCount: decision.urgentMessageIds.length,
appliedOperationCount: decision.operations.length + 1,
enforcementOperations: decision.operations.filter((operation) => operation.operation === "upsert_open_case"),
};
}

View file

@ -1,4 +1,5 @@
import { voidSelfStateOperationSchema, type VoidSelfStateOperation, type VoidSelfStateTypedProjection } from "@voidbot/core";
import { z } from "zod";
const ALLOWED_MODERATION_TAGS = new Set(["moderation:instaban", "moderation:strike", "moderation:case_only"]);
@ -14,6 +15,12 @@ export interface VoidModerationHeartbeatContext {
recentHistory: unknown;
}
export interface VoidModerationHeartbeatDecision {
reviewedMessageIds: string[];
urgentMessageIds: string[];
operations: VoidSelfStateOperation[];
}
export function projectVoidModerationHeartbeatContext(input: {
state: VoidSelfStateTypedProjection;
observedAt: Date;
@ -40,7 +47,8 @@ export function projectVoidModerationHeartbeatContext(input: {
export function buildVoidModerationHeartbeatPrompt(context: VoidModerationHeartbeatContext): string {
return [
"You are Void's rules-only public-community moderation organ. This is not public speech, private rumination, or repo thought.",
"Return only a JSON array of complete typed Void self-state operation objects. Do not use markdown fences, commentary, tools, or file writes.",
"Return only one JSON object with reviewedMessageIds, urgentMessageIds, and operations. Do not use markdown fences, commentary, tools, or file writes.",
"reviewedMessageIds must contain every message ID in recentHistory exactly once. urgentMessageIds is the subset requiring urgent safety accounting; use [] when none are urgent.",
"Allowed operations: upsert_open_case, close_open_case.",
"Each new case must identify one concrete source message and contain exactly one infringement:<type> tag plus exactly one moderation:instaban, moderation:strike, or moderation:case_only tag.",
"Choose the strongest supported infringement type; never multiply cases or sanctions for one message.",
@ -54,13 +62,36 @@ export function buildVoidModerationHeartbeatPrompt(context: VoidModerationHeartb
export function parseVoidModerationHeartbeatOperations(input: {
outputText: string;
state: VoidSelfStateTypedProjection;
}): VoidSelfStateOperation[] {
observedMessageIds: string[];
}): VoidModerationHeartbeatDecision {
const trimmed = input.outputText.trim();
const candidate = trimmed.match(/^```(?:json)?\s*([\s\S]*?)\s*```$/i)?.[1]?.trim() ?? trimmed;
let raw: unknown;
try { raw = JSON.parse(candidate); } catch (error) { throw new Error(`Moderation heartbeat output is not valid JSON: ${error instanceof Error ? error.message : String(error)}`); }
if (!Array.isArray(raw)) throw new Error("Moderation heartbeat output must be a JSON array.");
return raw.map((entry, index) => validateOperation(entry, index, input.state));
const envelope = z.object({
reviewedMessageIds: z.array(z.string().trim().min(1)),
urgentMessageIds: z.array(z.string().trim().min(1)),
operations: z.array(z.unknown()),
}).strict().parse(raw);
assertExactReviewLedger(input.observedMessageIds, envelope.reviewedMessageIds);
const observed = new Set(input.observedMessageIds);
if (envelope.urgentMessageIds.some((id) => !observed.has(id))) throw new Error("Moderation urgent ledger contains a message outside the observed evidence window.");
const operations = envelope.operations.map((entry, index) => validateOperation(entry, index, input.state));
const accounted = new Set([
...input.state.moderationCursor.openCases.map((entry) => entry.sourceMessageId),
...operations.flatMap((operation) => operation.operation === "upsert_open_case" ? [operation.case.sourceMessageId] : []),
]);
const unaccountedUrgent = envelope.urgentMessageIds.filter((id) => !accounted.has(id));
if (unaccountedUrgent.length > 0) throw new Error(`Urgent moderation evidence is unaccounted for: ${unaccountedUrgent.join(", ")}.`);
return { reviewedMessageIds: envelope.reviewedMessageIds, urgentMessageIds: envelope.urgentMessageIds, operations };
}
function assertExactReviewLedger(observed: string[], reviewed: string[]): void {
const expected = [...new Set(observed)].sort();
const actual = [...new Set(reviewed)].sort();
if (reviewed.length !== actual.length || expected.length !== actual.length || expected.some((id, index) => id !== actual[index])) {
throw new Error("Moderation review ledger must account for every observed message exactly once before cursor advancement.");
}
}
function validateOperation(entry: unknown, index: number, state: VoidSelfStateTypedProjection): VoidSelfStateOperation {

View file

@ -0,0 +1,16 @@
import type { loadConfig } from "@voidbot/config";
import { requestBoundedTextCompletion } from "./bounded-text-completion-actuator.js";
export async function projectVoidModerationOperations(input: {
prompt: string;
config: ReturnType<typeof loadConfig>;
model?: string;
}, dependencies: { fetch?: typeof fetch; readText?: (path: string) => Promise<string> } = {}): Promise<string> {
return requestBoundedTextCompletion({
prompt: input.prompt,
config: input.config,
purpose: "Void moderation heartbeat",
model: input.model ?? process.env.VOID_MODERATION_HEARTBEAT_MODEL,
maxCompletionTokens: Number(process.env.VOID_MODERATION_HEARTBEAT_MAX_COMPLETION_TOKENS) || 2048,
}, dependencies);
}

View file

@ -137,6 +137,9 @@ const envSchema = z.object({
BIFROST_CULTMESH_COMMAND_PUMP_ENABLED: booleanFromEnv.default(true),
REPO_FACE_HEARTBEATS_ENABLED: booleanFromEnv.default(false),
VOIDBOT_MODERATION_HEARTBEAT_ENABLED: booleanFromEnv.default(false),
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"),
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),
@ -248,6 +251,7 @@ export interface AppConfig {
pumpEnabled: boolean;
};
voidModerationHeartbeatEnabled: boolean;
voidModerationDaemon: { enabled: boolean; intervalMinutes: number; enforcementMode: string };
repoFaceHeartbeats: {
enabled: boolean;
statePath: string;
@ -538,6 +542,11 @@ export function loadConfig(env: NodeJS.ProcessEnv = process.env): AppConfig {
pumpEnabled: parsed.BIFROST_CULTMESH_COMMAND_PUMP_ENABLED,
},
voidModerationHeartbeatEnabled: parsed.VOIDBOT_MODERATION_HEARTBEAT_ENABLED,
voidModerationDaemon: {
enabled: parsed.VOID_MODERATION_DAEMON_ENABLED,
intervalMinutes: parsed.VOID_MODERATION_DAEMON_INTERVAL_MINUTES,
enforcementMode: parsed.VOID_MODERATION_ENFORCEMENT_MODE,
},
repoFaceHeartbeats: {
enabled: parsed.REPO_FACE_HEARTBEATS_ENABLED,
statePath: canonicalSchedulerStatePath(resolve(parsed.REPO_FACE_HEARTBEAT_STATE_PATH)),

View file

@ -86,6 +86,9 @@ import { runVoidMemoryMaintenance } from "../apps/persona-scheduler/dist/void-me
import { projectVoidMemoryOperations } from "../apps/persona-scheduler/dist/void-memory-text-actuator.js";
import { buildVoidModerationHeartbeatPrompt, parseVoidModerationHeartbeatOperations, projectVoidModerationHeartbeatContext } from "../apps/persona-scheduler/dist/void-moderation-heartbeat-projector.js";
import { readVoidModerationEvidence } from "../apps/persona-scheduler/dist/void-moderation-evidence-source.js";
import { runVoidModerationHeartbeat } from "../apps/persona-scheduler/dist/void-moderation-heartbeat-organ.js";
import { projectVoidModerationOperations } from "../apps/persona-scheduler/dist/void-moderation-text-actuator.js";
import { runVoidModerationEnforcement } from "../apps/persona-scheduler/dist/void-moderation-enforcement-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";
@ -844,23 +847,28 @@ const moderationContext = projectVoidModerationHeartbeatContext({
});
assert.match(JSON.stringify(moderationContext.recentHistory), /5 minutes ago/, "moderation evidence chronology is projected without surrendering exact operation time ownership");
assert.doesNotMatch(buildVoidModerationHeartbeatPrompt(moderationContext), /OPERATION_OUTPUT_PATH|apply-operation/, "portable moderation prompts return operations directly and cannot preserve the filesystem tool-writing contract");
const parsedModeration = parseVoidModerationHeartbeatOperations({ outputText: JSON.stringify([{
operation: "upsert_open_case",
case: {
sourceMessageId: "message-1", status: "pending", summary: "Concrete fixture evidence.", authorId: "human-1", authorName: "Human",
channelId: "room-1", whyItMatters: "The fixture crosses the safety boundary.", createdAt: physiologyObservedAt.toISOString(),
lastTouchedAt: physiologyObservedAt.toISOString(), tags: ["infringement:safety_threat", "moderation:instaban"],
},
}]), state: physiologyState });
assert.equal(parsedModeration[0]?.operation, "upsert_open_case", "moderation output parsing crosses the canonical operation schema and classification invariant");
assert.throws(() => parseVoidModerationHeartbeatOperations({ outputText: JSON.stringify([{
operation: "upsert_open_case",
case: {
sourceMessageId: "message-2", status: "watching", summary: "Ambiguous fixture evidence.", createdAt: physiologyObservedAt.toISOString(),
lastTouchedAt: physiologyObservedAt.toISOString(), tags: ["infringement:safety_threat", "infringement:weaponized_intimidation", "moderation:case_only"],
},
}]), state: physiologyState }), /exactly one infringement/, "moderation classification cannot multiply infringement authority for one message");
assert.throws(() => parseVoidModerationHeartbeatOperations({ outputText: JSON.stringify([{ operation: "update_sleep_cycle", sleepCycle: physiology.sleepCycle }]), state: physiologyState }), /not allowed/, "moderation cannot annex physiology even when the operation is schema-valid");
const parsedModeration = parseVoidModerationHeartbeatOperations({ outputText: JSON.stringify({
reviewedMessageIds: ["message-1"], urgentMessageIds: ["message-1"], operations: [{
operation: "upsert_open_case",
case: {
sourceMessageId: "message-1", status: "pending", summary: "Concrete fixture evidence.", authorId: "human-1", authorName: "Human",
channelId: "room-1", whyItMatters: "The fixture crosses the safety boundary.", createdAt: physiologyObservedAt.toISOString(),
lastTouchedAt: physiologyObservedAt.toISOString(), tags: ["infringement:safety_threat", "moderation:instaban"],
},
}],
}), state: physiologyState, observedMessageIds: ["message-1"] });
assert.equal(parsedModeration.operations[0]?.operation, "upsert_open_case", "moderation output parsing crosses the canonical operation schema and classification invariant");
assert.throws(() => parseVoidModerationHeartbeatOperations({ outputText: JSON.stringify({ reviewedMessageIds: ["message-2"], urgentMessageIds: [], operations: [{
operation: "upsert_open_case",
case: {
sourceMessageId: "message-2", status: "watching", summary: "Ambiguous fixture evidence.", createdAt: physiologyObservedAt.toISOString(),
lastTouchedAt: physiologyObservedAt.toISOString(), tags: ["infringement:safety_threat", "infringement:weaponized_intimidation", "moderation:case_only"],
},
}],
}), state: physiologyState, observedMessageIds: ["message-2"] }), /exactly one infringement/, "moderation classification cannot multiply infringement authority for one message");
assert.throws(() => parseVoidModerationHeartbeatOperations({ outputText: JSON.stringify({ reviewedMessageIds: ["message-1"], urgentMessageIds: [], operations: [{ operation: "update_sleep_cycle", sleepCycle: physiology.sleepCycle }] }), state: physiologyState, observedMessageIds: ["message-1"] }), /not allowed/, "moderation cannot annex physiology even when the operation is schema-valid");
assert.throws(() => parseVoidModerationHeartbeatOperations({ outputText: JSON.stringify({ reviewedMessageIds: [], urgentMessageIds: [], operations: [] }), state: physiologyState, observedMessageIds: ["message-1"] }), /every observed message/, "moderation cannot advance over an omitted message");
assert.throws(() => parseVoidModerationHeartbeatOperations({ outputText: JSON.stringify({ reviewedMessageIds: ["message-1"], urgentMessageIds: ["message-1"], operations: [] }), state: physiologyState, observedMessageIds: ["message-1"] }), /unaccounted/, "urgent classification cannot advance without an existing or proposed case");
let moderationExporterArgs: string[] = [];
const moderationEvidence = await readVoidModerationEvidence({ priorCursorTimestamp: "2026-07-16T02:00:00.000Z" }, {
run: async (_file, args) => {
@ -870,6 +878,52 @@ const moderationEvidence = await readVoidModerationEvidence({ priorCursorTimesta
});
assert.deepEqual(moderationExporterArgs.slice(-4), ["--after", "2026-07-16T02:00:00.000Z", "--limit", "120"], "moderation evidence acquisition owns cursor-bounded chronological export arguments");
assert.deepEqual(moderationEvidence.observedCursor, { lastReviewedMessageId: "message-1", lastReviewedTimestamp: "2026-07-16T02:55:00.000Z" }, "moderation evidence returns the exact parent-owned cursor witness separately from prompt chronology");
const moderationState = createEmptyVoidSelfState({ createdAt: "2026-07-15T00:00:00.000Z" });
const appliedModerationOperations: string[] = [];
let moderationModelCalls = 0;
const moderationOrganDependencies = {
loadState: async () => moderationState,
readEvidence: async () => moderationEvidence,
projectText: async () => {
moderationModelCalls += 1;
return JSON.stringify({ reviewedMessageIds: ["message-1"], urgentMessageIds: ["message-1"], operations: [{
operation: "upsert_open_case",
case: { sourceMessageId: "message-1", status: "pending", summary: "Concrete fixture evidence.", authorId: "human-1", createdAt: physiologyObservedAt.toISOString(), lastTouchedAt: physiologyObservedAt.toISOString(), tags: ["infringement:safety_threat", "moderation:instaban"] },
}] });
},
applyOperation: async (_store: unknown, operation: { operation: string; run?: { runner: string; ranAt: string; summary: string }; case?: unknown; lastReviewedMessageId?: string; lastReviewedTimestamp?: string }) => {
appliedModerationOperations.push(operation.operation);
if (operation.operation === "record_scheduled_run" && operation.run) moderationState.scheduledRuntime.lastRuns.push(operation.run);
if (operation.operation === "upsert_open_case" && operation.case) moderationState.moderationCursor.openCases.push(operation.case as never);
if (operation.operation === "record_reviewed_messages") {
moderationState.moderationCursor.lastReviewedMessageId = operation.lastReviewedMessageId;
moderationState.moderationCursor.lastReviewedTimestamp = operation.lastReviewedTimestamp;
}
return moderationState;
},
};
const moderated = await runVoidModerationHeartbeat({ statePath: "void-self-state.cc", rules: "Fixture rules", enforcementMode: "log_only", observedAt: physiologyObservedAt }, moderationOrganDependencies as never);
assert.equal(moderated.status, "ok", "portable moderation coordinator completes one reviewed evidence window");
assert.deepEqual(appliedModerationOperations, ["record_scheduled_run", "upsert_open_case", "record_reviewed_messages"], "moderation persists attempt and cases before the sole cursor commit");
const repeatedModeration = await runVoidModerationHeartbeat({ statePath: "void-self-state.cc", rules: "Fixture rules", enforcementMode: "log_only", observedAt: physiologyObservedAt }, moderationOrganDependencies as never);
assert.deepEqual(repeatedModeration, { status: "skipped", reason: "already_attempted_this_window", runnerId: "void-moderation-heartbeat-attempt:message-1" }, "the same evidence window cannot become a recurring inference loop");
assert.equal(moderationModelCalls, 1, "moderation makes at most one model call per latest-message evidence window");
let bannedUserId: string | undefined;
const moderationEnforcement = await runVoidModerationEnforcement({ statePath: "void-self-state.cc", mode: "policy", botToken: "token", guildId: "guild", observedAt: physiologyObservedAt }, {
loadState: async () => moderationState,
banMember: async ({ userId }) => { bannedUserId = userId; },
applyOperation: async (_store, operation) => {
if (operation.operation === "close_open_case") {
const existing = moderationState.moderationCursor.openCases.find((entry) => entry.sourceMessageId === operation.sourceMessageId);
if (existing) Object.assign(existing, { status: operation.status, resolvedAt: operation.resolvedAt, resolutionSummary: operation.resolutionSummary });
}
return moderationState;
},
});
assert.equal(bannedUserId, "human-1", "policy enforcement lowers an instaban through the injected Discord actuator");
assert.equal(moderationEnforcement.actions[0]?.status, "instaban_applied", "successful sanction closes the typed case with an enforcement result");
const repeatedEnforcement = await runVoidModerationEnforcement({ statePath: "void-self-state.cc", mode: "policy", botToken: "token", guildId: "guild", observedAt: physiologyObservedAt }, { loadState: async () => moderationState });
assert.equal(repeatedEnforcement.evaluatedCaseCount, 0, "resolved moderation debt cannot be sanctioned twice on later cadences");
const maintainedState = structuredClone(physiologyState);
let memoryModelCalls = 0;
const memoryMaintenanceDependencies = {
@ -901,6 +955,16 @@ const memoryModelOutput = await projectVoidMemoryOperations({ prompt: "Maintain
assert.equal(memoryModelOutput, "[]", "the portable memory actuator returns only model text to the operation parser");
assert.equal(memoryRequestBody?.model, "memory-model", "the memory actuator uses one explicit configured model and bounded completion budget");
assert.equal(memoryRequestBody?.tools, undefined, "memory maintenance cannot acquire filesystem or side-effect tools through the model actuator");
let moderationRequestBody: Record<string, unknown> | undefined;
const moderationModelOutput = await projectVoidModerationOperations({ prompt: "Review moderation evidence.", config: { openAiApi: { baseUrl: "https://model.invalid/v1", apiKey: "secret", model: "moderation-model", timeoutMs: 1000, authHeader: "Authorization", maxCompletionTokens: 512 } } as never }, {
fetch: async (_url, init) => {
moderationRequestBody = JSON.parse(String(init?.body));
return new Response(JSON.stringify({ choices: [{ message: { content: '{"reviewedMessageIds":[],"urgentMessageIds":[],"operations":[]}' } }] }), { status: 200 });
},
});
assert.match(moderationModelOutput, /reviewedMessageIds/, "the portable moderation actuator returns only model text to the decision parser");
assert.equal(moderationRequestBody?.model, "moderation-model", "the moderation actuator uses one explicit configured model and bounded completion budget");
assert.equal(moderationRequestBody?.tools, undefined, "moderation inference cannot acquire filesystem or side-effect tools");
const moderationLaunchCommand = buildVoidModerationLaunchCommand({
pendingMentionsPath: "C:/Void's state/pending.json",
runnerScript: "C:/VoidBot/scripts/run.ps1",