Extract moderation evidence source
This commit is contained in:
parent
4b9550ce06
commit
5b9bfd6a85
2 changed files with 91 additions and 0 deletions
|
|
@ -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<VoidModerationEvidence> {
|
||||
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<string, unknown>;
|
||||
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<string, unknown>;
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
|
@ -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 = {
|
||||
|
|
|
|||
Loading…
Reference in a new issue