Files
GMW/services/discord-gateway/AGENTS.md
T
mytheclipsebotreview 2b6ec59286 refactor(ai): remove semantic embedding cache + Qdrant vector store
Hapus seluruh fitur embedding/Qdrant (tidak dipakai lagi):

- gateway: drop embeddingClient.ts, qdrantClient.ts, archiveEmbedder.ts
  dan tes qdrantEnsure.test.ts; moderationOrchestrator kembali ke
  exact-hash cache -> LLM (tanpa phase-2 semantic lookup); textCacheStore
  kehilangan findSimilarTextModeration / parseQdrantVerdict /
  isSemanticBandAccepted / upsertBareKeyToQdrant; cache-prune hanya
  menyapu Postgres.
- backend: drop embed.ts + qdrant.ts, endpoint messages.semanticSearch
  dan schema/type terkait; kolom embedding dilepas dari schema
  text_analysis_cache.
- frontend: hapus toggle EXACT/SEMANTIC, hook useSemanticSearch,
  API client + tipe SemanticSearchResult.
- config: buang AI_LLM_EMBEDDING_* dan QDRANT_* (env + .env.example).
- docs: ARCHITECTURE.md / AGENTS.md / README.md / diagram arsitektur
  disesuaikan (LLM caller - vision, cache = exact-hash saja).

Verifikasi: tsc 0 (backend, gateway, frontend); bun test 135 pass +
37 pass, 0 fail; biome 0 error.
2026-09-25 01:38:51 +07:00

5.7 KiB

Discord Gateway — Agent Guide

Read ../../AGENTS.md first. This file adds gateway-specific conventions.

Event-driven microservice: captures Discord events, runs AI moderation, publishes to Redis.

Quick reference

pnpm typecheck              # tsc --noEmit
pnpm lint                   # biome check --diagnostic-level=error .
pnpm build                  # tsc
pnpm test                   # vitest run
pnpm format                 # biome format --write .

Architecture (Event-driven)

src/
├── index.ts                     # Entry point → initializeDiscordGateway()
├── app/
│   ├── bootstrap.ts             # Wires client, DB, Redis, workers, schedulers
│   ├── shutdown.ts              # Graceful shutdown
│   └── retention.ts             # Expired-record cleanup
├── shared/
│   ├── config/index.ts          # Zod-validated env (SINGLE source of truth)
│   ├── database/                # Drizzle ORM + pg Pool + migrations
│   ├── logger/index.ts          # pino + createChildLogger()
│   ├── errors/index.ts          # AppError hierarchy
│   ├── utils/                   # retry, pagination
│   ├── discord/clientOptions.ts # discord.js-selfbot-v13 options
│   ├── uploader.ts              # Attachment upload helper
│   ├── redis-channels.ts        # Redis channel-name constants
│   └── moderation-types.ts      # Shared AI analysis types
├── modules/
│   ├── ai-moderation/           # LLM moderation pipeline (largest module)
│   ├── message-capture/         # Discord event listeners + DB store
│   ├── attachment-upload/       # Download + sharp resize + upload
│   ├── event-broadcaster/       # Redis pub/sub publisher
│   ├── command-handler/         # Backend→gateway Redis commands
│   ├── reaction-tracking/       # Reaction events
│   ├── thread-tracking/         # Thread events
│   ├── user-presence/           # Presence/status events
│   ├── channel-topic/           # Channel topic events
│   ├── guild-member-events/     # Member join/leave
│   └── gateway-metrics/         # Prometheus /metrics (port 4016)
└── tests/                       # Vitest suites

Key invariants (DO NOT BREAK)

  1. LLM is the only judge. Failed LLM → status:"error" + recovery retry. Never reintroduce regex/heuristic content classification.
  2. Discord tokens sanitized before reaching LLM (discordTokens.ts).
  3. Streaming is mandatory against the router base URL.

AI moderation pipeline

aiAnalyzer.ts → batchScheduler.ts → batchProcessor.ts → individualFallbackProcessor.ts
    ↓                  ↓                    ↓                        ↓
moderationOrchestrator.ts → (hash cache → LLM)
    ↓                          ↓                              ↓
textBatchProcessor.ts    mediaBatchProcessor.ts         llmClient.ts
  • Entry: aiAnalyzer.ts (queueMessageAnalysis, startPendingAIAnalysisWorker)
  • Concurrency: two per-lane LLM semaphores (2026-09-24) — text (AI_LLM_MAX_CONCURRENT, default 8) and media/vision (AI_LLM_MEDIA_MAX_CONCURRENT, default 4); a media backlog can never consume text slots
  • Piscina: text pool (4 threads) + media pool (2 threads)
  • Locks are per conversation per lane (conversationProcessing maps key → lane → startedAt): the text lane of a conversation never waits on that conversation's media lane (this was the "image blocks the queue" bug)
  • Each worker thread has its own pg Pool (min 0, grows to POSTGRES_POOL_MAX)

Module: message-capture

  • messageCapture.ts — Discord event listeners (messageCreate/Update/Delete)
  • messageStore.ts — DB operations
  • messageMetadata.ts — metadata extraction
  • messagesDb.ts / messagesCrud.ts — DB schema operations
  • retentionDb.ts / reviewsDb.ts / attachmentsDb.ts — auxiliary tables

Redis channels (outbound to backend)

See src/shared/redis-channels.ts for canonical names. Examples:

discord:message:created, discord:moderation:action, discord:attachment:uploaded

Config (env vars)

All validated via Zod in shared/config/index.ts. Critical:

  • DISCORD_TOKEN — selfbot token
  • MONITOR_GUILD_ID — primary guild
  • DATABASE_URL — PostgreSQL
  • REDIS_URL — pub/sub to backend
  • AI_ANALYSIS_ENABLED — master toggle for AI moderation
  • AI_LLM_BASE_URL / AI_LLM_API_KEY — LLM router
  • PISCINA_MAX_THREADS / PISCINA_MEDIA_MAX_THREADS — worker pool sizing

Concurrency model

  • Main thread: event loop + LLM semaphore
  • Text Piscina pool: PISCINA_MAX_THREADS (default 4)
  • Media Piscina pool: PISCINA_MEDIA_MAX_THREADS (default 2)
  • Batch routed to media pool if ANY message has attachment/sticker/embed
  • Each worker thread initializes own pg Pool (min 0, grows to POSTGRES_POOL_MAX)
  • MemoryMax: 1G (service systemd limit)

Testing

  • Vitest. Test files: tests/<name>.test.ts
  • Run: pnpm test
  • Key test areas: batch operations, cache guards, image/video handling, context enrichment
  • Unit tests for pure helpers (classifier, normalize, parse), integration for pipeline stages

Common pitfalls

  • Piscina pool isolation: worker threads are NOT the main thread. Cannot share state via module-level variables. Use DB or Redis for cross-thread state.
  • Cache eviction: LRU caches (user metadata, term glossary) evict at max size. Don't assume cache hit after eviction.
  • Circuit breaker: per-conversation CB opens after repeated failures. Check circuitBreaker.ts state when debugging "missing analysis".
  • Gateway≠Backend schema: both have redis-channels.ts and moderation-types.ts. Keep them in sync manually.