Route moderation cursor and receipts through typed operations

This commit is contained in:
Meta Vens 2026-05-16 12:14:40 +01:00
commit 82f35b22a0
7 changed files with 76 additions and 223 deletions

View file

@ -1,4 +1,3 @@
import { createHash } from "node:crypto";
import { mkdir, readFile, writeFile } from "node:fs/promises";
import { dirname, resolve } from "node:path";
@ -223,78 +222,6 @@ export async function readModerationStateCursor(canonicalPath: string): Promise<
};
}
export async function setModerationStateCursor(
paths: ModerationStatePaths,
cursor: {
lastReviewedMessageId?: string | null;
lastReviewedTimestamp?: string | null;
},
): Promise<ModerationState> {
const canonicalPath = resolve(paths.canonicalPath);
const workingPath = resolve(paths.workingPath);
const state = await loadModerationState(canonicalPath);
state.moderation_runtime.cursor = {
lastReviewedMessageId: cursor.lastReviewedMessageId ?? null,
lastReviewedTimestamp: cursor.lastReviewedTimestamp ?? null,
};
await saveModerationState(canonicalPath, state);
await materializeModerationStateWorkingView({ state, workingPath });
return state;
}
export interface ModerationDeliveryReceipt {
sentAt: string;
mode?: string | null;
transport?: string | null;
channelId?: string | null;
replyToMessageId?: string | null;
personaName?: string | null;
personaAvatarUrl?: string | null;
contentLength?: number | null;
chunkCount?: number | null;
preview?: string | null;
}
export async function recordModerationDeliveryReceipt(
paths: ModerationStatePaths,
receipt: ModerationDeliveryReceipt,
): Promise<ModerationState> {
const canonicalPath = resolve(paths.canonicalPath);
const workingPath = resolve(paths.workingPath);
const state = await loadModerationState(canonicalPath);
const runtime = cloneJson(state.moderation_runtime);
const runtimeRecord = runtime as Record<string, unknown>;
const receipts = readArray(runtimeRecord, "recent_delivery_receipts")
.filter(isObject)
.filter((entry): entry is Record<string, unknown> => !!readString(entry, "sentAt"));
const normalizedReceipt = normalizeDeliveryReceipt(receipt);
const receiptKey = readString(normalizedReceipt, "receiptKey");
const existing = receipts.find(
(entry) =>
readString(entry, "receiptKey") === receiptKey ||
(readString(normalizedReceipt, "replyToMessageId") &&
readString(entry, "replyToMessageId") ===
readString(normalizedReceipt, "replyToMessageId") &&
readString(entry, "previewHash") === readString(normalizedReceipt, "previewHash")),
);
if (existing) {
Object.assign(existing, normalizedReceipt);
} else {
receipts.push(normalizedReceipt);
}
runtimeRecord.recent_delivery_receipts = receipts
.sort(compareDeliveryReceiptsBySentAt)
.slice(-24);
closeAnsweredOpenCases(runtimeRecord, normalizedReceipt);
state.moderation_runtime = runtime;
await saveModerationState(canonicalPath, state);
await materializeModerationStateWorkingView({ state, workingPath });
return state;
}
function createModerationStateCache(canonicalPath: string): CultCache {
const cache = new CultCache();
cache.registerDocumentType(moderationStateDocument);
@ -303,81 +230,6 @@ function createModerationStateCache(canonicalPath: string): CultCache {
return cache;
}
function normalizeDeliveryReceipt(receipt: ModerationDeliveryReceipt) {
const sentAt = receipt.sentAt;
const replyToMessageId = receipt.replyToMessageId ?? null;
const preview = receipt.preview?.trim() ?? null;
const previewHash = preview ? sha1(preview) : null;
const receiptKey = sha1(
JSON.stringify({
sentAt,
channelId: receipt.channelId ?? null,
replyToMessageId,
previewHash,
mode: receipt.mode ?? null,
}),
);
return {
sentAt,
mode: receipt.mode ?? null,
transport: receipt.transport ?? null,
channelId: receipt.channelId ?? null,
replyToMessageId,
personaName: receipt.personaName ?? null,
personaAvatarUrl: receipt.personaAvatarUrl ?? null,
contentLength: receipt.contentLength ?? null,
chunkCount: receipt.chunkCount ?? null,
preview,
previewHash,
receiptKey,
};
}
function compareDeliveryReceiptsBySentAt(left: unknown, right: unknown) {
const leftSentAt = isObject(left) ? readString(left, "sentAt") ?? "" : "";
const rightSentAt = isObject(right) ? readString(right, "sentAt") ?? "" : "";
return leftSentAt.localeCompare(rightSentAt);
}
function closeAnsweredOpenCases(runtime: Record<string, unknown>, receipt: Record<string, unknown>) {
const replyToMessageId = readString(receipt, "replyToMessageId");
if (!replyToMessageId) {
return;
}
const preview = readString(receipt, "preview");
const sentAt = readString(receipt, "sentAt");
const openCases = readArray(runtime, "open_cases").filter(isObject);
for (const openCase of openCases) {
const sourceMessageId = readString(openCase, "sourceMessageId");
const status = readString(openCase, "status");
if (sourceMessageId !== replyToMessageId) {
continue;
}
if (status && ["answered", "resolved", "closed", "retired", "dropped"].includes(status)) {
continue;
}
openCase.status = "answered";
openCase.resolvedAt = sentAt ?? null;
openCase.lastTouchedAt = sentAt ?? null;
openCase.resolutionSummary =
preview && preview.length > 0
? `Answered in-channel: ${preview}`
: "Answered in-channel.";
}
runtime.open_cases = openCases;
}
function sha1(value: string) {
return createHash("sha1").update(value).digest("hex");
}
function reattachSemanticVectors(previous: ModerationState, next: ModerationState): ModerationState {
const previousVectors = collectSemanticVectors(previous);
const merged = cloneJson(next);

View file

@ -204,6 +204,10 @@ function applyLegacyCompatibilityOperation(
}
case "record_delivery_receipt":
upsertLegacyRuntimeArray(runtime, "recent_delivery_receipts", operation.receipt, "receiptKey");
runtime.recent_delivery_receipts = readObjectArray(runtime.recent_delivery_receipts)
.filter((entry) => readString(entry, "sentAt"))
.sort((left, right) => (readString(left, "sentAt") ?? "").localeCompare(readString(right, "sentAt") ?? ""))
.slice(-24);
closeLegacyOpenCaseForReceipt(runtime, operation.receipt);
return true;
case "queue_candidate_intervention":