fix: break moderation infinite re-review loop by adding 'flagged' status
Some checks failed
CI / Typecheck & Lint (pull_request) Failing after 1s

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 <noreply@anthropic.com>
This commit is contained in:
Alexander Whitestone
2026-03-22 20:42:13 -04:00
parent 42b8826d18
commit ce3d6ffb4d
4 changed files with 14 additions and 4 deletions

View File

@@ -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<ModerationDecision> {
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", {

View File

@@ -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,