Commit Graph
191 Commits
Author SHA1 Message Date
asepharyanaandClaude Opus 5 (Nous Research) f82b5caae4 refactor(gateway): strip boilerplate fields from few-shot examples
The 32 few-shot examples each re-echoed score/confidence/
recommended_action/categories/policy_version inline (~150 chars ×
32). Those fields carry zero moderation-decision signal — the schema
and their ??-default coercion already live in OUTPUT_INSTRUCTIONS +
moderationResponseParser.ts. Removed 96 redundant key/value pairs.

Kept per-example: message_id, status, flags, severity, evidence,
analysis — the fields that actually teach decisions. Parser derives
the rest via ?? fallback, so real output shape is unchanged.

examples.ts: 21.7K→18.5K chars; FEW_SHOT(mixed) 15.3K→13.4K.
Total mixed system prompt now 33.9K (was 39.3K at audit start,
~14% leaner). tsc + 129 tests + biome green.

Co-Authored-By: Claude Opus 5 (Nous Research)
2026-08-16 09:00:55 +07:00
asepharyanaandClaude Opus 5 (Nous Research) 9e2b107fcd refactor(gateway): compact AI analysis system prompt, preserve all rules
- prompts/system.ts: merge 3 overlapping framing blocks (Blok Data /
  Konteks Pengguna / Framing Konteks vs Target) into 1 tight block —
  same coverage, no duplicated "standalone judgment / profile-is-
  reference-not-evidence" prose.
- prompts/output.ts: trim duplicated user_history/standalone paragraph
  in PERSONALITY & MEMORI (keep concrete per-case lessons).
- prompts/examples.ts: drop 2 exact-duplicate-lesson few-shots (LGBT id=19
  dup of id=30; weapons-tech id=33 dup of id=32). All teaching signals
  retained via the surviving example of each lesson.

Static system prompt: text 32.7K→29.2K, mixed 39.3K→35.8K chars
(~10% smaller). No moderation rule, zero-tolerance category, or decision
tree altered — accuracy-controlling content untouched. tsc + 129 tests +
biome green.

Co-Authored-By: Claude Opus 5 (Nous Research)
2026-08-16 08:52:11 +07:00
asepharyana 6244e307a3 feat: surface AI analysis duration across gateway, backend, and FE
Adds per-message AI moderation analysis time (ai_analysis_duration_ms)
so operators can see how long the LLM took to moderate each message.

Gateway:
- messagesTable: new ai_analysis_duration_ms (bigint) column.
- AIAnalysisUpdate + buildAIAnalysisSet: carry analysisDurationMs through
  both single and bulk update paths.
- ai-analysis-worker: measure wall-clock time around runModerationAnalysis
  and attach it to every result in the batch.

Backend:
- Mirror schema column; messageMapper maps ai_analysis_duration_ms;
  moderation-types + MappedMessage expose it.

Frontend:
- message.ts type gains ai_analysis_duration_ms.
- AiBadge (messages view) shows 'status · 1.2s' when duration is present;
  analysis view badge mirrors the same formatting.

DB:
- scripts/add-ai-analysis-duration.sql (idempotent ADD COLUMN IF NOT EXISTS).

No behavior change for moderation logic; null until new gateway build
records values.
2026-08-16 00:11:00 +07:00
asepharyana 2d7c7f2c35 fix(gateway): stop Qdrant upsert aborts (semantic cache was being skipped)
Qdrant upserts were failing with 'This operation was aborted' ~32x/2h,
so semantic moderation cache entries were silently dropped. Root cause:
upsertQdrantPoint ran ensureQdrantCollection() on EVERY call — a GET
(and sometimes DELETE+PUT) round-trip — while the request AbortController
had only a 10s timeout. Under moderation load Qdrant is busy (the
gmw_text_moderation collection is not yet HNSW-indexed, so searches are
full-scans), the extra round-trips pushed the upsert past 10s, and the
client aborted it.

- Memoise ensureQdrantCollection() at module scope so the collection is
  verified exactly once per process (resetQdrantCollectionCache() for
  tests / config reload).
- Bump the upsert request timeout 10s -> 30s so a transiently busy
  Qdrant no longer aborts the write.

Qdrant server itself is healthy (<100ms for direct upsert; collection is
green), so no server-side change is needed. Semantic cache should now
populate reliably.
2026-08-15 23:40:19 +07:00
asepharyana 416c690ebc style(gateway,backend): clear all biome warnings (no warnings left behind)
Address every remaining biome lint/format warning across both services
so the codebase ships warning-free:

- textCacheStore: drop unused deleteExpiredQdrantPoints import; hash
  image cache key (sha256[:32]) so long/base64 URLs no longer blow the
  text_analysis_cache PK B-tree 8191-byte index (was aborting the media
  analysis lock INSERT).
- bootstrap: drop unused unhandledRejection promise param.
- moderationOrchestrator: drop unused  destructure at L197.
- mediaDownloader / textBatchProcessor / transmitter: replace non-null
  assertions with proper null guards (stickerName ?? '', urlImages.get
  guard, backpressureQueue.shift guard).
- backend utils: throw lastError ?? fallback instead of lastError!.
- message-capture: remove unused  (retentionDb),  (moderationActionsDb,
  reviewsDb); simplify renderDiscordMentions guard to optional chain.
- transmitter: remove dead write-only  field + its assignments.

No behavior change beyond the cache-key hashing (now deterministic
fixed-length) and the intentional null-safety guards.
2026-08-15 23:18:33 +07:00
asepharyana 9c83ec86cc fix(gateway): image vision analysis + media cache lock failures
Two root causes behind 'all image analysis failing':

1. imageResizer still emitted lossless PNG for vision input. A 1024px
   Facebook photo balloons to multi-MB PNG base64 that the vision model
   silently rejects ('Vision API null response'). Switch to JPEG q85
   (no upscaling) — same photo drops to ~100-400KB, model processes fine.
   Re-encodes even already-small images so raw originals never bloat the
   data URL. Added tests/imageResizer.test.ts covering both cases.

2. acquireMediaAnalysisLock INSERT aborted with 'index row requires N
   bytes, maximum size is 8191'. text_analysis_cache.text is the PK in a
   B-tree index (8191-byte/row cap); callers pass the raw image URL as the
   key, and base64 data URLs / very long URLs blow past the limit, so the
   lock INSERT fails and every media analysis is skipped. Hash the URL in
   makeImageCacheKey (image:<sha256[:32]>) — fixed-length, deterministic,
   well under the limit. All store/get/lock/delete callers already route
   through this function so lookup stays consistent.
2026-08-15 23:04:52 +07:00
asepharyana e2013988ff ci: fix biome format gate so Build & Deploy passes
Auto-format llmClient.ts (Object.assign indent) — the only biome
error blocking the Build & Deploy workflow. Logic unchanged; gateway
biome check now exits 0 (11 pre-existing warnings remain, non-blocking).
2026-08-15 21:39:44 +07:00
asepharyana 9ae26b8ec9 refactor(llm): unify vision routing with text moderation and remove dedicated endpoint 2026-08-15 21:05:06 +07:00
asepharyana 7ebee7559d feat(llm): add disableThinking option for faster LLM analysis and update config 2026-08-15 20:52:53 +07:00
asepharyana 6c9a91dad4 style(vision): biome format llmClient.ts (wrap long const line) 2026-08-15 14:38:44 +07:00
asepharyana bcb563ea7f feat(vision): route multimodal analysis to dedicated NVIDIA direct endpoint
- config: add AI_LLM_VISION_BASE_URL + AI_LLM_VISION_API_KEY (separate from text router)
- llmClient: llmVision() now calls dedicated vision endpoint when configured
  (axios POST to integrate.api.nvidia.com, model nvidia/nemotron-3-nano-omni-30b-a3b-reasoning,
  reasoning_budget 16384, non-stream), falls back to router combo otherwise
- keeps text/moderation on omniroute, vision on NVIDIA direct
2026-08-15 14:31:53 +07:00
asepharyana d3cb5f6756 refactor: rombak cache AI analisis image — pakai CDN URL langsung, hapus phash+sha
- Cache key image = CDN URL (query params stripped), bukan SHA data URL
  → re-analysis SAME attachment selalu cache-hit, berbeda attachment tidak kolisi
- Hapus perceptual hash (imghash dep + phash get/upsert/compute) sepenuhnya
- Hapus makeImageCacheKey hashing, ganti makeImageCacheKey yang return CDN URL
- textCacheStore, visionAnalyzer, mediaCache, mediaAnalysisClient updated
- imghash dependency removed from package.json
- Purge 82 stale cache rows (image: + phash:) dari DB
2026-08-12 22:31:08 +07:00
asepharyana 37787cc4f0 fix: prevent false positive moderation on physics/tech discussions
- Add examples for technical discussions (kinetic energy, drone weapon
  engineering, physics simulations) that should be marked clean
- System rule: physics/engineering topics (kinetik, gravitasi, energi,
  drone, senjata, drone warfare, CAD, CNC, 3D printing, robotics, aerospace)
  are safe when in technical context — flag only if explicit threat
- Riwayat pengguna dengan pelanggaran sebelumnya tidak memengaruhi
  penilaian pesan bersih yang terpisah dan tidak mengandung pelanggaran
2026-08-12 22:12:11 +07:00
asepharyana f849a87f2f fix: remove user history injection to prevent false positive moderation
- Removed getUserRecentInfractions usage in textBatchProcessor.ts and visionAnalyzer.ts
- Removed buildUserHistoryXml import and calls
- Messages are now evaluated standalone, not influenced by past violations in other channels
- Updated moderation prompts with clearer instructions about user_history usage
- Fixes issue where benign messages like 'tubuh manusia vs gravitasi' were incorrectly flagged due to carryover from previous drone weapons discussion

The user history context was causing the LLM to interpret unrelated current messages
as threats because it conflated them with past violations. Now each message is judged
on its own merit with only channel-specific context.
2026-08-12 20:48:26 +07:00
asepharyana 3b221823e7 feat(gateway): add observability logging for vision cache hits/misses
Add debug logging to trace cacheKey + messageId + content length on
every vision cache HIT and MISS, so we can detect if the vision model
returns duplicate analysis for different images (provider issue vs
cache collision). Includes the phash on cache miss (new analysis cached).

Follow-up to 9f7ce7d which fixed makeImageCacheKey to hash full data
URL instead of just first 128 chars (root cause of all images sharing
the same cached 'konten judi' verdict due to hash collision).
2026-08-12 19:22:56 +07:00
asepharyana 9f7ce7dbd5 fix(gateway): hash full image data URL for cache key to prevent collision
Root cause: makeImageCacheKey() only hashed the first 128 chars of the
data URL. Since all resized images use the same MIME prefix
('data:image/png;base64,') + identical base64 header bytes, nearly every
image got the same 16-char hash → 'image:<same-hash>' → all images reused
the first cached vision analysis (often a gambling-detection verdict).

Fix: hash the entire data URL instead of just the prefix. Verified
114 stale 'image:' entries + 745 stale 'phash:' entries purged from prod
DB. tsc --noEmit clean, 133 tests pass.
2026-08-12 18:28:19 +07:00
asepharyana d9f5592e6e feat(glossary): persist resolved definitions in Postgres + harden live SearXNG lookups
- Add term_glossary_cache table + migration 0014: resolved definitions are
  stored permanently (definitions rarely change); misses stay ephemeral in
  Redis/LRU with 1h TTL so transient failures get retried
- Lookup flow: LRU -> Redis -> Postgres (permanent) -> live SearXNG; DB hits
  re-warm the fast caches; stale Redis miss sentinels no longer shadow DB
- Rate-limit-aware live lookups: concurrency 2 + stagger, retry once on empty
  results, strict definition filter (Wikipedia preferred, rejects
  disambiguation/ads/translate-homepages)
- Make SEARXNG_BASE_URL configurable via env (default unchanged)
2026-08-12 14:22:22 +07:00
asepharyana f70a92880e feat(glossary): implement term glossary for LLM moderation with caching and extraction logic 2026-08-12 13:44:52 +07:00
asepharyana c18431bdbf fix(ai-moderation): never cache vision outputs that claim 'no image seen'
Root cause (3rd layer after 50371bd + 4f4c435): a vision model run
(2026-08-10) returned 'Maaf, saya tidak melihat gambar apapun yang terlampir...'
and that text was cached as a VALID vision_llm result (image + phash keys,
24h/7d TTL). Every subsequent analysis of the same image (same hash/phash)
hit the poisoned cache, so image analysis looked broken forever even though
9router responded fine — the moderation LLM wrote 'lampiran yang gagal
terbaca' from a cache hit.

Also: mimo via 9router streams reasoning in delta.reasoning +
delta.reasoning_details[].text (content:"") — extractChunkText only read
delta.reasoning_content, so those runs aggregated empty → 'Vision API null
response' (observed 08:54/09:07/09:38).

Fixes:
- llmClient.extractChunkText: fall back to delta.reasoning and
  reasoning_details[].text (mimo), on top of reasoning_content (gemma).
- visionAnalyzer: isNoImageSeenText() detects 'no image' style outputs;
  such results are NEVER cached, and poisoned entries are purged when hit
  (LRU/DB/phash) so re-analysis actually re-runs vision.
- Tests: reasoning/reasoning_details extraction + isNoImageSeenText
  (Indonesian + English, no false positives on real descriptions).
2026-08-11 09:55:43 +07:00
asepharyana 4f4c43555f fix(ai-moderation): attachment-upload race dropped images before vision
Root cause (2nd layer after 50371bd): the analysis worker could pick up an
image message while its attachment upload was still in flight
(upload_status='pending'). downloadAndExtractFrame then fell back to the
Discord CDN URL (cdn.discordapp.com), which often 404s for old/purged links,
and 'if (!res.ok) return' silently dropped the image — no log, no vision
call, empty image map, and the LLM produced a text-only verdict like
'lampiran yang gagal terbaca oleh sistem'.

Fixes:
- ai-analysis-worker: skip targets whose attachment upload is still pending
  (both batch + individual paths) — they stay ai_status='pending' and the
  next 15s cycle analyzes them after the upload lands.
- mediaDownloader.downloadAndExtractFrame: try uploaded_url first, then
  discord_url as fallback; log non-OK responses (status + host) instead of
  silently returning; log when all candidate URLs fail.
2026-08-11 09:44:34 +07:00
asepharyana 50371bd2d1 fix(ai-moderation): read delta.reasoning_content in stream aggregation — image vision never returned text
Root cause: 9router combo 'multimodal' routes to cloudflare-ai/@cf/google/
gemma-4-26b-a4b-it which streams ALL output in delta.reasoning_content
(content:"") and finishes with 'length' at max_tokens. llmClient only read
delta.content, so llmVision returned empty → every image moderation fell back
to text-only analysis ('Meskipun analisis gambar gagal' in every ai_analysis).

Fix: extractChunkText() prefers delta.content then falls back to
delta.reasoning_content (also handles message/text/response fields), with
unit tests for the exact 9router chunk shape. Verified live against a real
DB image: oc/mimo-v2.5-free (new first model in the multimodal combo) returns
a proper description in delta.content.
2026-08-11 08:06:28 +07:00
asepharyana 65c9c2cd9e feat(ai-moderation): enrich analysis context with recency, repetition, user history and channel topic
- <message> targets now carry time (ISO), repetitions (N identical short texts = spam signal), bot and edited flags; escape id/user XML
- rich <user_reputation>: total_infractions, clean_streak, last_offense_days_ago, repeat_offender (7-day window)
- <user_history> with last flagged messages for repeat offenders (wires dead getUserRecentInfractions)
- <user_profile as_of> staleness signal; <location_context topic> from captured channel topic
- prompt framing + output instructions teach the LLM to use the new signals without treating history as proof
- tests: contextEnrichment.test.ts (13) + topic cases in conversationContext.test.ts
2026-08-10 17:15:33 +07:00
asepharyana 0a5254bf20 feat(ai-moderation): enhance context handling with structured XML blocks and user profiles 2026-08-10 16:46:55 +07:00
asepharyana 185d81f0e0 feat(ai-moderation): reset offensive nickname instead of deleting message
When the ONLY violation is offensive_username (message content clean):
- Message is NOT deleted (nickname-only violation bypasses auto-delete)
- Member's server nickname is reset to default username via
  setNickname(null) (Discord shows the global username again)
- Action 'reset_nickname' logged to moderation_actions; cooldown
  10min per guild:user (LRU) so repeated messages by same member
  don't hammer the Discord PATCH
- Config: AUTO_NICKNAME_RESET_ENABLED / AUTO_NICKNAME_RESET_COOLDOWN_MS
2026-08-10 11:48:27 +07:00
asepharyana ecbb538c9f feat(ai-moderation): use per-server nickname (displayName) in analysis payload
- resolveDisplayName(): member.displayName from captured metadata,
  falls back to global username
- Applied to context lines, target message blocks, and media message
  blocks — LLM sees the name the channel actually sees (nickname can
  carry moderation signal itself)
2026-08-10 11:36:37 +07:00
asepharyana 4049ab4201 feat(ai-moderation): rich context + link media vision analysis
- Conversation context recency gates (GAP_MS/MAX_AGE_MS): drop stale
  messages before silence gaps; cold_start anchor + flow descriptor
  tells LLM whether conversation is ongoing or restarted
- [location] block: channel name, thread name, nsfw/age flags from
  captured metadata (thread names instead of bare IDs)
- Link media -> multimodal: text-batch URL fetches that resolve to
  images now run vision analysis (bounded 15s) and switch prompt to
  mixed mode; <web_content> gains og:title for page context
- pnpm-workspace.yaml: approve sharp build script (unblocks install)
2026-08-10 11:26:26 +07:00
asepharyana 6293d588bc chore(lint): biome cleanup across services — format, sort imports, drop unused
- discord-gateway: 74 lint errors -> 0 (format, import sorting, unused
  imports/vars, dead breath var)
- backend: format + sort imports (11 warnings left: noExplicitAny)
- frontend: remove unused imports, drop dead breathing var, fix
  useExhaustiveDependencies (scroll keyed on messages), a11y biome-ignore
  for drag surface + stopPropagation container (mouse-only gestures)
- remaining warnings are false positives: index keys on static lists,
  <img> in static export (next/image unsupported), noExplicitAny

tsc --noEmit clean on all 3 services; vitest green (60+36).
2026-08-01 22:09:02 +07:00
asepharyana 0ef2b715c4 fix(gateway): enable stream for all LLM calls — router always streams SSE
Build & Deploy (Nix) / build-and-deploy (proxy) (push) Successful in 3m36s
Build & Deploy (Nix) / build-and-deploy (backend) (push) Successful in 4m9s
Build & Deploy (Nix) / build-and-deploy (discord-gateway) (push) Successful in 11m29s
Audit lanjutan: 6x 'LLM API request failed: Request was aborted' per jam.
Root cause: 9router/omniroute SELALU balas SSE (data: chunks) walau request
tanpa stream:true — SDK OpenAI non-stream menunggu FULL body sebelum parse,
jadi batch moderasi besar yang upstream-nya lambat kena timeout 30-60s dan
di-abort. llmClient sudah punya agregasi streaming (chunks → ChatCompletion).

Fix: stream:true di llmCaller (moderasi batch/individual), llmVision,
cultureLearner, userProfileLearner. Verified: SDK stream test 806ms vs
sebelumnya abort. Caller lain (recovery worker dll) lewat llmCaller sama.
2026-08-01 15:00:59 +07:00
asepharyana dfe689bdec fix(gateway): mediaAnalysis ffprobe path, fallback error-log, generic closer sanitize
Build & Deploy (Nix) / build-and-deploy (proxy) (push) Successful in 3m47s
Build & Deploy (Nix) / build-and-deploy (backend) (push) Successful in 4m11s
Build & Deploy (Nix) / build-and-deploy (discord-gateway) (push) Successful in 11m36s
Audit log produksi (sejak deploy13:38) menemukan 3 isu:
1. mediaDownloader.ts spawn /usr/bin/ffprobe + /usr/bin/ffmpeg (path keras) —
   ENOENT di Nix karena binary cuma di ffmpeg-headless closure. Pakai
   PATH-resolved ('ffprobe'/'ffmpeg') seperti voice-recording module
   (ffmpegProcess.ts/transmitter.ts) — 5 media warning hilang.
2. individualFallbackProcessor log error 'Success' di level50 tiap fallback
   BERHASIL (logModerationError dengan new Error('Success')) — ganti
   logger.info dengan verdict yang sama; error log cuma untuk error asli.
3. moderationResponseParser: strip frasa penutup generik ('Tidak ada
   indikasi pelanggaran.') yang masih sering dikeluarkan LLM walau prompt
   melarang (277/1486 analisis mengandung frasa, termasuk hari ini).
   sanitizeGenericCleanCloser hanya mencocok frasa di AKHIR, teks substantif
   tetap utuh. Unit test: 6/6 pass.
2026-08-01 14:00:43 +07:00
asepharyana 1f91f99de3 feat(automod): render sticker, role & user names in moderation views
QoL lanjutan dari fix60084b3: content pesan mentah masih nampilin
snowflake (<@&roleid>, <@userid>, <:emoji:id>) di log moderasi dan
prompt LLM. Sekarang dirender ke nama yang bisa dibaca:

- Gateway capture: metadata menyimpan mentionedRoles + mentionedUsers
  (id+name) dari message.mentions, disimpan ke metadata JSON
- renderDiscordMentions(): <@&id> -> @RoleName, <@id> -> @Username,
  <:name:id> -> :name:, fallback @role/@user — dipakai di
  conversationContext (konteks LLM) dan moderationBuilders
  (getAnalysisContent) sehingga LLM lihat nama role/user beneran,
  bukan placeholder generik
- Frontend renderMessageContent() (mirror gateway) dipasang di semua
  tempat nampilin content: message-card, message-detail(-view),
  search-overlay, search-panel, users/channels section, live-stream,
  mod-queue, review list; sticker-only message tetap [Sticker: name],
  pesan teks+sticker kini ikut nampilin nama sticker
- tsc --noEmit PASS di gateway & frontend; renderDiscordMentions
  diverifikasi manual (6 kasus: role/user/emoji/unknown/plain)
2026-08-01 08:56:10 +07:00
Developer 6df4f306dd refactor: remove unused text analysis module and integrate Qdrant enhancements
Build & Deploy (Nix) / build-and-deploy (backend) (push) Successful in 2m30s
Build & Deploy (Nix) / build-and-deploy (discord-gateway) (push) Successful in 3m7s
Build & Deploy (Nix) / build-and-deploy (proxy) (push) Successful in 3m20s
- Deleted the text analysis prompt constants and helpers as they are no longer needed.
- Added batch search functionality for Qdrant to optimize vector searches.
- Implemented methods for deleting expired Qdrant points and invalidating cache based on content hash.
- Updated text batch processor to use new timeout configurations and modified content building for moderation prompts.
- Enhanced text cache store to support new Qdrant integration and improved cache invalidation logic.
- Introduced a new user reputation model with a more nuanced trust scoring system, including penalties and rewards for user behavior.
- Added unit tests for the new trust model to ensure correctness of penalty and trust gain calculations.
- Updated configuration schema to reflect new timeout settings and removed deprecated OpenAI moderation keys.
2026-07-31 23:09:00 +07:00
Developer fc475dfbb7 feat(automod): store semantic cache embeddings in Qdrant
Build & Deploy (Nix) / build-and-deploy (backend) (push) Successful in 3m7s
Build & Deploy (Nix) / build-and-deploy (discord-gateway) (push) Successful in 2m21s
Build & Deploy (Nix) / build-and-deploy (proxy) (push) Successful in 2m33s
New qdrantClient.ts (zero-dep fetch REST): ensure collection with cosine
distance (auto-recreate on vector-size change), upsert point w/ verdict
payload, search w/ expires_at filter + score threshold.

textCacheStore: when QDRANT_URL set, embeddings are upserted to Qdrant
(primary) and searched there first; Postgres embedding column remains as
legacy fallback for pre-Qdrant rows. Config: QDRANT_URL/COLLECTION/API_KEY.
QDRANT_URL already in repo .env; added to VPS env + GATEWAY_ENV secret.
2026-07-31 21:30:43 +07:00
Developer 7ab9a7fd2d fix(automod): force float encoding for embeddings — Nvidia models reject base64
Build & Deploy (Nix) / build-and-deploy (backend) (push) Successful in 1m35s
Build & Deploy (Nix) / build-and-deploy (discord-gateway) (push) Successful in 2m24s
Build & Deploy (Nix) / build-and-deploy (proxy) (push) Successful in 2m51s
OpenAI SDK v6 defaults to encoding_format=base64; llama-nemotron-embed
(Nvidia-backed) returns 400 'do not support base64'. Semantic cache was
silently disabled in prod. encoding_format: 'float' fixes it.
2026-07-31 20:14:18 +07:00
Developer 8480407167 fix(automod): parenthesize ?? chain in autoDeleteNotify — Node runtime SyntaxError
Build & Deploy (Nix) / build-and-deploy (backend) (push) Successful in 3m4s
Build & Deploy (Nix) / build-and-deploy (discord-gateway) (push) Successful in 2m22s
Build & Deploy (Nix) / build-and-deploy (proxy) (push) Successful in 2m31s
TS compiled this fine, but the JS spec forbids mixing || and ??
without explicit parens; Node threw 'Unexpected token ??' at startup,
crash-looping gmw-discord-gateway (restart counter 250). Wrap the
fallback chain in parens so the expression is valid.
2026-07-31 19:46:07 +07:00
Developer 1249ae81d8 perf(automod): compress prompts ~40% + semantic cache via AI_LLM_EMBEDDING_MODEL
Build & Deploy (Nix) / build-and-deploy (backend) (push) Successful in 3m4s
Build & Deploy (Nix) / build-and-deploy (discord-gateway) (push) Successful in 2m29s
Build & Deploy (Nix) / build-and-deploy (proxy) (push) Successful in 2m33s
Prompt overhaul (token-frugal, same quality):
- rules.ts 28KB -> 10.3KB: every normative rule kept (safe lists, SARA
  6 kategori, LGBT/Israel zero tolerance, anti-evasion, decision tree,
  evasi hierarchy, image rules) with duplicated phrasing removed
- examples.ts 24.7KB -> 20KB: all 31 teaching examples kept; analysis
  strings shortened, redundant categories/policy_version dropped from
  example outputs (both optional in the response schema)
- output.ts 13.8KB -> 6.8KB: compressed schema + personality + format
  rules; CRITICAL bans on generic analysis and reply-context requirement
  retained
- system.ts: MEDIA_INSTRUCTIONS compressed, key rules kept

Semantic moderation cache (AI_LLM_EMBEDDING_MODEL):
- New embeddingClient.ts: OpenAI-compatible embeddings + cosine
  similarity; degrades gracefully when model/key unset
- textCacheStore: stores embedding JSON per verdict, findSimilarTextModeration
  reuses near-duplicate verdicts (min 0.97 cosine, processing locks skipped)
- moderationOrchestrator: after exact-hash miss, embed text-only targets
  and reuse stored verdict for near-duplicates -> skips expensive chat
  completion for spam variants; fresh verdicts written back with embedding
- Config: AI_LLM_EMBEDDING_MODEL / MIN_SIMILARITY (0.97) / MAX_CANDIDATES (30)
- Migration 0012: ADD COLUMN embedding to text_analysis_cache (idempotent)
- .env.example documents the new vars
2026-07-31 19:37:53 +07:00
Developer 60084b3cc3 fix(automod): flow real LLM analysis + descriptive fallback
Build & Deploy (Nix) / build-and-deploy (backend) (push) Successful in 3m2s
Build & Deploy (Nix) / build-and-deploy (discord-gateway) (push) Successful in 2m25s
Build & Deploy (Nix) / build-and-deploy (proxy) (push) Successful in 2m40s
Root cause: ai-analysis-worker read llmResult.explanation and
llmResult.toxicityScore — fields the LLM pipeline never produces
(canonical AnalysisResult uses analysis/score). Every message fell back
to the bare template "Tidak ada indikasi pelanggaran." and the stored
score was always 0.

- Map analysis/score correctly; fallback now quotes the message content
- Prompt: ban generic analysis phrasing, require reply context
- LLM context: include replied-to message content (metadata.reference)
  so the model can explain what the user is replying to
- Frontend: show thread/channel names from metadata instead of raw IDs
  (message card, detail views, search overlay); detail panel now
  displays the ai_analysis text
- Auto-delete log/DM include the descriptive analysis as the reason
2026-07-31 19:11:13 +07:00
Developer 0bd4369ae9 refactor(automod): remove regex classifier — LLM is the sole judge
Build & Deploy (Nix) / build-and-deploy (backend) (push) Successful in 3m2s
Build & Deploy (Nix) / build-and-deploy (discord-gateway) (push) Successful in 2m20s
Build & Deploy (Nix) / build-and-deploy (proxy) (push) Successful in 2m26s
Delete fastClassifier.ts (manual regex patterns for phone/email/IP/crypto/
spam/toxicity) and simpleFallback.ts. These hardcoded patterns were the
source of false positives (Discord emoji snowflakes matched phone_number,
URL digits matched phone, etc.) and produced heuristic verdicts whenever
the LLM failed.

New flow: Message → LLM (with conversation context, media evidence, user
reputation) → verdict. On LLM failure the message is marked 'error' and
retried by the recovery worker — no heuristic verdicts, ever.

Discord markdown tokens (custom emoji/mentions/timestamps) are normalized
to readable placeholders ([emoji:name], @user, @role, #channel, [time])
before reaching the LLM via discordTokens.ts.
2026-07-31 17:55:02 +07:00
Developer a2cda745f7 fix(automod): sanitize Discord tokens + boundary phone regex in Layer 1
Build & Deploy (Nix) / build-and-deploy (backend) (push) Successful in 3m2s
Build & Deploy (Nix) / build-and-deploy (discord-gateway) (push) Successful in 2m21s
Build & Deploy (Nix) / build-and-deploy (proxy) (push) Successful in 2m29s
Custom emoji (<:name:id>), user/role/channel mentions and timestamps embed
long numeric snowflakes that tripped the phone_number / personal_info /
ip_address_sharing patterns — e.g. <:mambotongue:1463255254220148939> was
flagged as phone_number. Strip Discord markdown tokens before pattern
matching and require phone matches to not sit inside a longer digit run.
2026-07-31 17:25:00 +07:00
Developer dcd13482c2 refactor: break monorepo into 3 standalone services (gateway, backend, frontend)
Build & Deploy / build-and-push (backend) (push) Failing after 35s
Build & Deploy / build-and-push (discord-gateway) (push) Failing after 25s
Build & Deploy / build-and-push (proxy) (push) Failing after 25s
- Remove pnpm workspace, moon repo, and all monorepo tooling
- Delete packages/shared/, embed shared code directly into each service
- Copy packages/shared/src/* -> services/backend/src/shared/ and services/discord-gateway/src/shared/
- Replace all @bete/shared imports with @/shared/ path alias
- Remove @bete/shared workspace dependency from both services
- Update root package.json scripts from --filter to --prefix
- Rewrite Dockerfiles to build each service standalone
- Clean up biome.json, .gitignore, remove root drizzle.config.ts
2026-07-30 11:50:48 +07:00
DeveloperandClaude Opus 4.8 540a71f983 fix: resolve gateway build failures - type cast + exclude archive/
Build & Deploy / build-and-push (backend) (push) Successful in 25s
Build & Deploy / build-and-push (proxy) (push) Successful in 3m46s
Build & Deploy / build-and-push (discord-gateway) (push) Failing after 4m42s
- Cast llmResult through unknown to handle type mismatch between
  shared AnalysisResult and layer-specific local type
- Exclude src/**/archive/** from tsconfig to prevent dead code errors

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-28 15:35:43 +07:00
DeveloperandClaude Opus 4.8 59dc27733e fix: replace invalid \U escapes in ZALGO regex with RegExp constructor
Build & Deploy / build-and-push (discord-gateway) (push) Failing after 28s
Build & Deploy / build-and-push (backend) (push) Successful in 1m46s
Build & Deploy / build-and-push (proxy) (push) Successful in 1m37s
\U escapes are not valid in JavaScript/TypeScript regex literals.
Use new RegExp() constructor to avoid TS parser issues.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-28 15:10:47 +07:00
DeveloperandClaude Opus 4.8 (1M context) <noreply@anthropic.com 3f4fa42098 Merge branch worktree-neo-surveillance-redesign into main — Neo Surveillance redesign
Full frontend redesign with glassmorphic dark theme, floating top nav,
Live2D mascot, split-pane messages, and Ops Center dashboard.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com
2026-07-28 14:32:47 +07:00
DeveloperandClaude Opus 4.8 5802d02e29 refactor: large codebase cleanup - consolidate schemas, migrate to Drizzle ORM, extract frontend components, modernize Docker builds
Build & Deploy / build-and-push (discord-gateway) (push) Failing after 2m22s
Build & Deploy / build-and-push (backend) (push) Failing after 3m22s
Build & Deploy / build-and-push (proxy) (push) Successful in 1m36s
Build & Deploy / deploy (push) Skipped
- Consolidate all DB schema definitions into packages/shared as single source of truth
- Migrate backend from raw SQL to Drizzle ORM across all modules
- Extract frontend inline UI into separate component files
- Refactor discord-gateway circuitBreaker into conversationState + moderationState
- Convert messageStore to Proxy singleton pattern
- Add validateBody/validateQuery middleware + Zod schemas for API endpoints
- Modernize Docker builds with multi-stage + pnpm deploy
- Migrate CI/CD from deployment to image-based pipeline
- Remove 60+ unused/dead files (~15K lines)
- Update color scheme from sky-blue to teal-cyan
- Move DB connection management to @bete/shared/database

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-27 21:54:31 +07:00
asepharyana eca0a378f9 fix: increase max length for user profile summary and sanitize function
Deploy to VPS / deploy (push) Successful in 2m19s
2026-07-26 17:27:51 +07:00
asepharyana 0a6a9fd982 feat: update dependencies and improve dashboard functionality
Deploy to VPS / deploy (push) Failing after 1m43s
- Added new dependencies for Next.js and lucide-react in pnpm-workspace.yaml.
- Refactored DashboardPage component to improve readability and error handling.
- Enhanced Header component to display error status with an alert icon.
- Updated MobileTabBar and Sidebar components to use a centralized tabs definition.
- Improved ChannelsView in dashboard-panel to handle channel fetching more cleanly.
- Fixed ActiveSpeaker type to use camelCase for userId.
- Updated MessagesPanel to handle guildId checks more gracefully.
- Adjusted API calls in dashboard and messages to align with backend expectations.
- Refined type definitions across various interfaces for consistency and clarity.
2026-07-26 14:27:36 +07:00
asepharyana cedce85087 ci: fix lint errors for CI
Deploy to VPS / deploy (push) Successful in 1m47s
- Fix noImplicitAnyLet: add type to let match variable
- Fix noAssignInExpressions: use matchAll() + for-of instead of while
- Suppress useExhaustiveDependencies in mascot scroll effect
- Suppress useSemanticElements for message card click handler
2026-07-26 12:01:51 +07:00
asepharyana 27e929580e feat: update components and hooks to use get_untracked for improved performance 2026-07-04 03:03:36 +07:00
asepharyanaandworkflow agents ade5d6a7c3 fix: backend and discord-gateway improvements
- Update shared database schema
- Add shared utils
- Refactor backend middleware, auth routes, and dashboard repository
- Improve media analysis client with better error handling
- Fix searxng search URL construction
- Update URL fetcher for robustness

Co-authored-by: workflow agents
2026-07-02 06:02:07 +07:00
asepharyana 7efaf00c93 Revert "feat: migrate frontend to Astro + expand AI moderation + backend admin/runtime config"
This reverts commit d59b59a7a7.
2026-07-02 03:54:44 +07:00
asepharyana d59b59a7a7 feat: migrate frontend to Astro + expand AI moderation + backend admin/runtime config
Frontend:
- migrate from Vite to Astro (astro.config.mjs, pages/, layouts/)
- add admin panel, settings page, command palette, error boundary
- refactor App.tsx, MascotChatbot, Sidebar, Header, DashboardLayout
- update API client, WebSocket, auth, dashboard features

Backend:
- add admin module and config routes
- refactor middlewares, Redis connection, WebSocket server/bridge
- add runtime config loader

Discord Gateway:
- refactor AI moderation: circuit breaker, concurrency limiter, fallback processor
- add media analysis client, Seaxng search, user profile learner
- add new drizzle migration

Shared:
- extend database schema, add new config fields
2026-07-02 00:02:41 +07:00