Extract moderation heartbeat projection
This commit is contained in:
parent
1fdeb59aed
commit
4b9550ce06
2 changed files with 131 additions and 0 deletions
|
|
@ -0,0 +1,102 @@
|
|||
import { voidSelfStateOperationSchema, type VoidSelfStateOperation, type VoidSelfStateTypedProjection } from "@voidbot/core";
|
||||
|
||||
const ALLOWED_MODERATION_TAGS = new Set(["moderation:instaban", "moderation:strike", "moderation:case_only"]);
|
||||
|
||||
export interface VoidModerationHeartbeatContext {
|
||||
mode: "moderation_heartbeat";
|
||||
operationTimestamp: string;
|
||||
enforcementMode: string;
|
||||
rules: string;
|
||||
priorCursor: unknown;
|
||||
observedCursor: unknown;
|
||||
openCases: unknown[];
|
||||
urgentModerationWitnesses: unknown[];
|
||||
recentHistory: unknown;
|
||||
}
|
||||
|
||||
export function projectVoidModerationHeartbeatContext(input: {
|
||||
state: VoidSelfStateTypedProjection;
|
||||
observedAt: Date;
|
||||
observedCursor: unknown;
|
||||
recentHistory: unknown;
|
||||
urgentModerationWitnesses: unknown[];
|
||||
enforcementMode: string;
|
||||
rules: string;
|
||||
}): VoidModerationHeartbeatContext {
|
||||
const relative = <T>(value: T): T => projectRelativeChronology(value, input.observedAt) as T;
|
||||
return {
|
||||
mode: "moderation_heartbeat",
|
||||
operationTimestamp: input.observedAt.toISOString(),
|
||||
enforcementMode: input.enforcementMode,
|
||||
rules: input.rules,
|
||||
priorCursor: relative(input.state.moderationCursor),
|
||||
observedCursor: relative(input.observedCursor),
|
||||
openCases: relative(input.state.moderationCursor.openCases),
|
||||
urgentModerationWitnesses: relative(input.urgentModerationWitnesses),
|
||||
recentHistory: relative(input.recentHistory),
|
||||
};
|
||||
}
|
||||
|
||||
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.",
|
||||
"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.",
|
||||
"Do not reopen an already-actioned case. Urgent safety evidence must be accounted for by an existing case or a proposed upsert.",
|
||||
"Use operationTimestamp when a fresh exact timestamp is required. Relative chronology in evidence is not schema-ready timestamp text.",
|
||||
"Moderation context:",
|
||||
JSON.stringify(context, null, 2),
|
||||
].join("\n\n");
|
||||
}
|
||||
|
||||
export function parseVoidModerationHeartbeatOperations(input: {
|
||||
outputText: string;
|
||||
state: VoidSelfStateTypedProjection;
|
||||
}): VoidSelfStateOperation[] {
|
||||
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));
|
||||
}
|
||||
|
||||
function validateOperation(entry: unknown, index: number, state: VoidSelfStateTypedProjection): VoidSelfStateOperation {
|
||||
const operation = voidSelfStateOperationSchema.parse(entry);
|
||||
if (operation.operation !== "upsert_open_case" && operation.operation !== "close_open_case") {
|
||||
throw new Error(`Moderation heartbeat operation ${index + 1} is not allowed: ${operation.operation}.`);
|
||||
}
|
||||
if (operation.operation === "close_open_case") return operation;
|
||||
const prior = state.moderationCursor.openCases.find((item) => item.sourceMessageId === operation.case.sourceMessageId);
|
||||
if (prior && ["answered", "resolved", "closed", "retired", "dropped"].includes(prior.status)
|
||||
&& /(?:instaban|three-strike ban|strike \d\/3 recorded|ban applied)/i.test(prior.resolutionSummary ?? "")) {
|
||||
throw new Error(`Moderation heartbeat cannot reopen already-actioned case '${operation.case.sourceMessageId}'.`);
|
||||
}
|
||||
const tags = operation.case.tags.map((tag) => tag.toLowerCase());
|
||||
const infringementTags = tags.filter((tag) => tag.startsWith("infringement:"));
|
||||
const moderationTags = tags.filter((tag) => ALLOWED_MODERATION_TAGS.has(tag));
|
||||
if (infringementTags.length !== 1) throw new Error("Moderation heartbeat open cases must include exactly one infringement:<type> tag.");
|
||||
if (moderationTags.length !== 1) throw new Error("Moderation heartbeat open cases must include exactly one moderation classification tag.");
|
||||
return operation;
|
||||
}
|
||||
|
||||
function projectRelativeChronology(value: unknown, observedAt: Date, key = ""): unknown {
|
||||
if (Array.isArray(value)) return value.map((entry) => projectRelativeChronology(entry, observedAt));
|
||||
if (value && typeof value === "object") return Object.fromEntries(Object.entries(value).map(([entryKey, entry]) => [entryKey, projectRelativeChronology(entry, observedAt, entryKey)]));
|
||||
if (typeof value === "string" && /(?:at|timestamp)$/i.test(key) && Number.isFinite(Date.parse(value))) return relativeTime(value, observedAt);
|
||||
return value;
|
||||
}
|
||||
|
||||
function relativeTime(value: string, observedAt: Date): string {
|
||||
const difference = observedAt.getTime() - Date.parse(value);
|
||||
const future = difference < 0;
|
||||
const minutes = Math.max(0, Math.round(Math.abs(difference) / 60_000));
|
||||
const phrase = (amount: number, unit: string) => future ? `in ${amount} ${unit}${amount === 1 ? "" : "s"}` : `${amount} ${unit}${amount === 1 ? "" : "s"} ago`;
|
||||
if (minutes < 60) return phrase(minutes, "minute");
|
||||
const hours = Math.round(minutes / 60);
|
||||
if (hours < 24) return phrase(hours, "hour");
|
||||
const days = Math.round(hours / 24);
|
||||
return phrase(days, "day");
|
||||
}
|
||||
|
|
@ -84,6 +84,7 @@ import { runVoidPhysiologyOrgan } from "../apps/persona-scheduler/dist/void-phys
|
|||
import { buildVoidMemoryMaintenancePrompt, parseVoidMemoryMaintenanceOperations, projectVoidMemoryMaintenanceContext } from "../apps/persona-scheduler/dist/void-memory-maintenance-projector.js";
|
||||
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 { 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";
|
||||
|
|
@ -831,6 +832,34 @@ const parsedMaintenance = parseVoidMemoryMaintenanceOperations(JSON.stringify([{
|
|||
}]));
|
||||
assert.equal(parsedMaintenance[0]?.operation, "prune_short_term_memories", "memory output parsing crosses the canonical operation schema before application");
|
||||
assert.throws(() => parseVoidMemoryMaintenanceOperations(JSON.stringify([{ operation: "update_sleep_cycle", sleepCycle: physiology.sleepCycle }])), /not allowed/, "memory maintenance cannot annex physiology operations even when they are schema-valid");
|
||||
const moderationContext = projectVoidModerationHeartbeatContext({
|
||||
state: physiologyState,
|
||||
observedAt: physiologyObservedAt,
|
||||
observedCursor: { lastReviewedMessageId: "message-1", lastReviewedTimestamp: physiologyObservedAt.toISOString() },
|
||||
recentHistory: { messages: [{ id: "message-1", timestamp: "2026-07-16T02:55:00.000Z", content: "fixture" }] },
|
||||
urgentModerationWitnesses: [],
|
||||
enforcementMode: "log_only",
|
||||
rules: "Fixture rules",
|
||||
});
|
||||
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 maintainedState = structuredClone(physiologyState);
|
||||
let memoryModelCalls = 0;
|
||||
const memoryMaintenanceDependencies = {
|
||||
|
|
|
|||
Loading…
Reference in a new issue