From ce3d6ffb4d507ed81032ee5ddc8d51ea03244895 Mon Sep 17 00:00:00 2001 From: Alexander Whitestone Date: Sun, 22 Mar 2026 20:42:13 -0400 Subject: [PATCH] fix: break moderation infinite re-review loop by adding 'flagged' status When AI flags an event, transition status to 'flagged' instead of leaving it as 'pending'. This prevents processPending() from picking up the same flagged events every 30-second poll cycle and burning AI tokens indefinitely. - Add 'flagged' to QUEUE_STATUSES enum in schema - Set status='flagged' in autoReview() when AI flags an event - Include flagged count in admin stats endpoint - Add index on relay_event_queue.status for efficient queries Fixes #27 Co-Authored-By: Claude Opus 4.6 --- artifacts/api-server/src/lib/moderation.ts | 6 +++--- artifacts/api-server/src/routes/admin-relay.ts | 1 + lib/db/migrations/0007_moderation_flagged_status.sql | 9 +++++++++ lib/db/src/schema/relay-event-queue.ts | 2 +- 4 files changed, 14 insertions(+), 4 deletions(-) create mode 100644 lib/db/migrations/0007_moderation_flagged_status.sql diff --git a/artifacts/api-server/src/lib/moderation.ts b/artifacts/api-server/src/lib/moderation.ts index 5dca1a3..ca9d665 100644 --- a/artifacts/api-server/src/lib/moderation.ts +++ b/artifacts/api-server/src/lib/moderation.ts @@ -134,7 +134,7 @@ export class ModerationService { /** * Review a single pending event with Claude. * Returns "approve" (event is injected into strfry + status → auto_approved) - * or "flag" (status stays pending — admin must decide). + * or "flag" (status → flagged — admin must decide). */ async autoReview(eventId: string): Promise { const rows = await db @@ -176,10 +176,10 @@ export class ModerationService { if (result.decision === "approve") { await this.decide(eventId, "auto_approved", result.reason, "timmy_ai"); } else { - // Update reason but leave status as "pending" for admin + // Transition to "flagged" so processPending() won't re-review this event await db .update(relayEventQueue) - .set({ reviewReason: result.reason, reviewedBy: "timmy_ai" }) + .set({ status: "flagged", reviewReason: result.reason, reviewedBy: "timmy_ai" }) .where(eq(relayEventQueue.eventId, eventId)); logger.info("moderation: event flagged for admin review", { diff --git a/artifacts/api-server/src/routes/admin-relay.ts b/artifacts/api-server/src/routes/admin-relay.ts index d268312..df95fbe 100644 --- a/artifacts/api-server/src/routes/admin-relay.ts +++ b/artifacts/api-server/src/routes/admin-relay.ts @@ -106,6 +106,7 @@ router.get("/admin/relay/stats", requireAdmin, async (_req: Request, res: Respon res.json({ pending: statusMap["pending"] ?? 0, + flagged: statusMap["flagged"] ?? 0, approved: statusMap["approved"] ?? 0, autoApproved: statusMap["auto_approved"] ?? 0, rejected: statusMap["rejected"] ?? 0, diff --git a/lib/db/migrations/0007_moderation_flagged_status.sql b/lib/db/migrations/0007_moderation_flagged_status.sql new file mode 100644 index 0000000..649ed33 --- /dev/null +++ b/lib/db/migrations/0007_moderation_flagged_status.sql @@ -0,0 +1,9 @@ +-- Migration: Add 'flagged' status to relay_event_queue +-- Fixes infinite re-review loop (#27): AI-flagged events now transition to +-- status='flagged' instead of staying 'pending', so processPending() skips them. +-- +-- The status column is plain TEXT (not a Postgres enum), so no ALTER TYPE is +-- needed. This migration adds an index for admin queries on flagged events. + +CREATE INDEX IF NOT EXISTS idx_relay_event_queue_status + ON relay_event_queue(status); diff --git a/lib/db/src/schema/relay-event-queue.ts b/lib/db/src/schema/relay-event-queue.ts index f601fcc..46355bd 100644 --- a/lib/db/src/schema/relay-event-queue.ts +++ b/lib/db/src/schema/relay-event-queue.ts @@ -3,7 +3,7 @@ import { nostrIdentities } from "./nostr-identities"; // ── Status + reviewer types ─────────────────────────────────────────────────── -export const QUEUE_STATUSES = ["pending", "approved", "rejected", "auto_approved"] as const; +export const QUEUE_STATUSES = ["pending", "approved", "rejected", "auto_approved", "flagged"] as const; export type QueueStatus = (typeof QUEUE_STATUSES)[number]; export const QUEUE_REVIEWERS = ["timmy_ai", "admin"] as const; -- 2.43.0