From 5b9bfd6a85ba9500f433f00f51f05e09ec2d8302 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 16 Jul 2026 05:00:14 +0100 Subject: [PATCH] Extract moderation evidence source --- .../src/void-moderation-evidence-source.ts | 81 +++++++++++++++++++ scripts/smoke-persona-initiative-engine.ts | 10 +++ 2 files changed, 91 insertions(+) create mode 100644 apps/persona-scheduler/src/void-moderation-evidence-source.ts diff --git a/apps/persona-scheduler/src/void-moderation-evidence-source.ts b/apps/persona-scheduler/src/void-moderation-evidence-source.ts new file mode 100644 index 0000000..8ef4cc6 --- /dev/null +++ b/apps/persona-scheduler/src/void-moderation-evidence-source.ts @@ -0,0 +1,81 @@ +import { execFile } from "node:child_process"; +import { resolve } from "node:path"; +import { promisify } from "node:util"; + +const execFileAsync = promisify(execFile); + +export interface ModerationHistoryMessage { + id: string; + timestamp: string; + channelId?: string; + authorId?: string; + authorName?: string; + content?: string; + [key: string]: unknown; +} + +export interface VoidModerationEvidence { + status: "ok"; + after?: string; + hours?: number; + totalMatchingMessages: number; + returnedMessages: number; + messages: ModerationHistoryMessage[]; + observedCursor?: { lastReviewedMessageId: string; lastReviewedTimestamp: string }; +} + +export async function readVoidModerationEvidence(input: { + priorCursorTimestamp?: string; + limit?: number; + fallbackHours?: number; + scriptPath?: string; + nodeExecutable?: string; +}, dependencies: { + run?: (file: string, args: string[], options: { cwd: string; maxBuffer: number }) => Promise<{ stdout: string }>; +} = {}): Promise { + const scriptPath = resolve(input.scriptPath ?? "scripts/export-recent-discord-history.mjs"); + const args = [scriptPath]; + if (input.priorCursorTimestamp) args.push("--after", input.priorCursorTimestamp); + else args.push("--hours", String(input.fallbackHours ?? 6)); + args.push("--limit", String(input.limit ?? 120)); + const run = dependencies.run ?? (async (file, commandArgs, options) => execFileAsync(file, commandArgs, options)); + const { stdout } = await run(input.nodeExecutable ?? process.execPath, args, { cwd: process.cwd(), maxBuffer: 4 * 1024 * 1024 }); + let raw: unknown; + try { raw = JSON.parse(stdout); } catch (error) { throw new Error(`Moderation evidence exporter returned invalid JSON: ${error instanceof Error ? error.message : String(error)}`); } + const parsed = parseExporterResult(raw); + const latest = parsed.messages.at(-1); + return { + status: "ok", + after: parsed.after, + hours: parsed.hours, + totalMatchingMessages: parsed.totalMatchingMessages, + returnedMessages: parsed.messages.length, + messages: parsed.messages, + observedCursor: latest ? { lastReviewedMessageId: latest.id, lastReviewedTimestamp: latest.timestamp } : undefined, + }; +} + +function parseExporterResult(raw: unknown): { + after?: string; + hours?: number; + totalMatchingMessages: number; + messages: ModerationHistoryMessage[]; +} { + if (!raw || typeof raw !== "object" || Array.isArray(raw)) throw new Error("Moderation evidence exporter result must be an object."); + const value = raw as Record; + if (!Array.isArray(value.messages)) throw new Error("Moderation evidence exporter result has no messages array."); + const messages = value.messages.map((entry, index) => { + if (!entry || typeof entry !== "object" || Array.isArray(entry)) throw new Error(`Moderation evidence message ${index + 1} must be an object.`); + const message = entry as Record; + if (typeof message.id !== "string" || !message.id || typeof message.timestamp !== "string" || !Number.isFinite(Date.parse(message.timestamp))) { + throw new Error(`Moderation evidence message ${index + 1} requires id and valid timestamp.`); + } + return message as ModerationHistoryMessage; + }); + return { + after: typeof value.after === "string" ? value.after : undefined, + hours: typeof value.hours === "number" ? value.hours : undefined, + totalMatchingMessages: typeof value.totalMatchingMessages === "number" ? value.totalMatchingMessages : messages.length, + messages, + }; +} diff --git a/scripts/smoke-persona-initiative-engine.ts b/scripts/smoke-persona-initiative-engine.ts index 68d1e89..6db5aad 100644 --- a/scripts/smoke-persona-initiative-engine.ts +++ b/scripts/smoke-persona-initiative-engine.ts @@ -85,6 +85,7 @@ import { buildVoidMemoryMaintenancePrompt, parseVoidMemoryMaintenanceOperations, import { runVoidMemoryMaintenance } from "../apps/persona-scheduler/dist/void-memory-maintenance-organ.js"; 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 { 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"; @@ -860,6 +861,15 @@ assert.throws(() => parseVoidModerationHeartbeatOperations({ outputText: JSON.st }, }]), 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"); +let moderationExporterArgs: string[] = []; +const moderationEvidence = await readVoidModerationEvidence({ priorCursorTimestamp: "2026-07-16T02:00:00.000Z" }, { + run: async (_file, args) => { + moderationExporterArgs = args; + return { stdout: JSON.stringify({ after: "2026-07-16T02:00:00.000Z", totalMatchingMessages: 1, messages: [{ id: "message-1", timestamp: "2026-07-16T02:55:00.000Z", channelId: "room-1", content: "fixture" }] }) }; + }, +}); +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 maintainedState = structuredClone(physiologyState); let memoryModelCalls = 0; const memoryMaintenanceDependencies = {