- pickBatchWithinBudget: stop di overflow pertama (break), bukan skip —
batch tetap prefix kronologis tanpa gap analisis di tengah timeline.
Diekstrak ke batchBudget.ts (pure, estimator di-inject) + regression test.
- callModerationLLM: param opsional maxTokens; text/media caller menghitung
ceiling dari estimasi prompt (floor 2048, cap 16384) — batch kecil tak
lagi reserve window completion 16k.
- getPending/IncompleteMessagesByConversation: sort hasil UPDATE..RETURNING
by created_at ASC — Postgres tak menjamin urutan, konsumen (anchor konteks
messages[0], prefix batch) bergantung pada urutan kronologis.
- normalizeStoredStatus(): exact-hash & semantic (Qdrant/PG) cache reader
sebelumnya menipiskan 'warn' jadi 'flagged'/'clean' (type narrowing
legacy clean|flagged) — merusak gating auto-delete & label dashboard.
Kini status tersimpan dipertahankan penuh (clean/warn/flagged).
- prompts: hapus referensi <user_history> yang tak pernah di-inject,
SearXNG -> Wikipedia (sudah migrasi), referensi section yang tak ada,
typo 'secifik', dan baris list rusak '|-'.
- moderationBuilders: buang dead code buildUserProfilesBlock/
buildUserProfileRef/UserProfileEntry/buildUserHistoryXml (tanpa caller
produksi sejak context minimization) + test-nya.
- test baru: tests/storedStatusNormalization.test.ts (regresi warn).
upload.asepharyana.my.id redirect ke file mp3 tunggal; generic extractor
yt-dlp expose format ID '0' sehingga '-f bestaudio' gagal 'Requested
format is not available'. Chain bestaudio[ext=m4a]/bestaudio/best tetap
dapat m4a di YouTube dan jatuh ke 'best' untuk direct file.
- Recordings: custom RecordingAudioPlayer (play/pause, buffering spinner,
click-to-seek, time label, eq bars, single-playback antar kartu) +
highlight kartu now-playing
- Media: thumbnail di disc hero + queue row, equalizer saat playing,
badge 'up next', label Paused vs Now playing
- MiniPlayer global di AppFrame (fixed bottom-right, hidden on /media)
menggantikan use-media-player.tsx dead provider (dihapus)
- Voice: mic level meter live (AnalyserNode RMS) + slider mic/listen volume
tsc reported 'Property some does not exist on type {}' on doc.tags
because Drizzle jsonb inference returns a generic object. Cast explicitly.
This unblocks the GMW GitHub Actions deploy (test job).
The Drizzle schema used pgText('tags').array() which emits a Postgres
text[] column, but the migration defines tags as jsonb. The mismatch caused
INSERT/LIST on materi_documents to throw INTERNAL_SERVER_ERROR (500) because
Drizzle sent a text[] where the column expected jsonb.
- schema.ts: tags → pgJsonb('tags').notNull().default('[]')
- migration: dropped+recreated to match schema (jsonb, ms epoch defaults,
owner_user_id default 'anonymous')
Two independent WS handlers (useMessagesWsSync for message_created/
updated/analyzed, and useMessagesStream for message_snapshot) both
prepend live messages to the SWR list without enforcing order.
When frames arrive out-of-order (common with batched WS delivery),
the message feed gets scrambled.
Fix: add sortMessages() helper that sorts newest-first by created_at
(the list's stored order before .reverse() for display) and apply it
in every patchLists/mutate updater: message_created, message_updated,
message_analyzed, message_snapshot, and useLoadMore page appends.
Function declaration is hoisted so useLoadMore (defined above the
helper) can use it.
The fix-imports.mjs script blindly appended '.js' to every @/ alias
import, even when the source specifier already carried a .js
extension (e.g. '@/shared/config/index.js'). This produced
'index.js.js' in the emitted dist/, causing ERR_MODULE_NOT_FOUND
at startup.
This was latent: only triggered once digestScheduler.ts (which
uses @/shared/config/index.js with explicit extension) was built.
The user-reputation removal (2a8f6d9) was also blocked by this
bug — stale binary kept crashing with 'user_reputations' query
errors because it was never redeployed.
Fix: only append .js when the @/ specifier has no existing
extension. Applied to both gateway and backend scripts.
SearXNG was already replaced by Wikipedia REST/Action APIs (wikipediaClient.ts).
Update comments to reflect the current implementation: term glossary now
resolves definitions via Wikipedia → Redis → Postgres cache chain, with no
SearXNG dependency.
Discord GoLive sends screen-share audio on a separate SSRC from the
user's microphone. In @discordjs/voice v0.19, VoiceReceiver.onUdpMessage
silently drops packets for SSRCs not in ssrcMap (which is only populated
from VOICE_STATE_UPDATE/VOICE_SERVER_UPDATE). This caused screen-share
audio to never trigger receiver.speaking and never reach the speakingHandler.
Fix: hookScreenShareAudio() wraps onUdpMessage to:
1. Detect incoming RTP packets with unknown SSRCs (OPRUS payload type 120)
2. Infer the owning userId by proximity to known audioSSRC
3. Clone the user's VoiceUserData into ssrcMap under the new SSRC
4. Let the original handler decrypt and forward to the subscription stream
5. Listen on ssrcMap 'create'/'update' events for video SSRC changes
Also removes the broken initial approach (polling ssrcMap which never
contains screen-share SSRCs).
Automated public weekly summary: top categories/domains/channels + coverage rate, posted to configured webhook. Uses getDatabase() direct query (no oRPC HTTP dependency), guards one-fire-per-week on restart.
Gateway @/ alias imports use no .js extension (relative imports
keep .js). The .js suffix on @/ paths caused double-extension
ERR_MODULE_NOT_FOUND (embeddingClient.js.js) at runtime.
- Persist structured verdict (flags/severity/confidence/evidence) on
moderation_actions so the public web can show WHY a message was moderated.
- Add a persistent Qdrant archive collection (gmw_message_archive); embed
every captured message at capture time (fire-and-forget, best-effort).
- Public semantic search over the archive (backend oRPC + FE toggle on the
messages view). Both features are read-only/public and fully automatic.
Migration: 0015_add_moderation_explainability.sql
- Memoize buildSystemPrompt by (mode|channelCulture); identical signatures
now reuse the ~5k-token core instead of rebuilding per sub-batch call
(textBatchProcessor rebuilt it inside the loop; a 200-msg batch re-sent
the full system prompt ~4x). Correction tail stays per-attempt (uncached).
- Hoist URL-image -> vision evidence out of the per-sub-batch loop in
textBatchProcessor: it depends only on fetched images + full target set,
so compute once per whole batch, not per sub-batch.
- Compact system instructions: collapse 3x-duplicated 'evaluate by content
alone' statements into one standalone rule; trim output.ts channel-culture
+ context framing already covered by rules.ts/system.ts; drop duplicate
programming-error-log few-shot (id 17, covered by rules AMAN list).
- Fix misleading config default: AI_LLM_BASE_URL default -> omniroute
(gateway already runs omniroute via BWS; 9router was dead/misleading).
typecheck + lint + build green.
- Add shared skeleton building blocks (SkeletonHero, SkeletonMetricRow,
SkeletonPanel, SkeletonRows) and wire them into every view's initial
loading branch, replacing bare spinners for a cohesive shimmering shell.
- Polish EmptyState with a glow-ring icon chip instead of a flat icon.
- Harden .light theme: color-scheme, tuned scrollbar + selection for pale
canvas. Dark/light theme toggle (next-themes) already persisted in TopBar.
- resetOffensiveNickname: skip when target role sits above bot
(member.manageable) instead of hammering a doomed setNickname PATCH
that Discord rejects with 50013 'Missing Permissions'. Log the
Discord error code on failure for clear diagnosis.
- llmCaller: include contentPreview (first 200 chars) in the parse-
failure warning so non-JSON LLM responses are debuggable.
- Add wikipediaClient.ts: native fetch to Wikipedia REST/Action APIs
(search + summary), no extra npm dependency.
- Extract shared Redis cache into cacheStore.ts (decoupled from search).
- Term glossary now uses wikipediaSummary for direct article lookup.
- Remove searxngSearch.ts entirely; drop SEARXNG_BASE_URL config,
add WIKIPEDIA_LANG / WIKIPEDIA_TIMEOUT_MS.
- Rename backend searxngCalls metric to webSearchCalls.
User: 'jangan ada reputasi juga' — no profile, no reputation in the prompt,
raw messages only.
- textBatchProcessor: drop initializeUserReputation fetch + <user_reputation>
tag injection (kept the minimal <message> tag + reply/reference context).
- visionAnalyzer (prepareMediaMessage): same removal.
- prompts/system.ts + prompts/output.ts: replace <user_reputation>/<user_history>
instructions with an explicit 'no per-user profile/reputation context'
note so the LLM judges purely on message content + conversation/web/location.
- mediaBatchProcessor: fix stale comment.
Trust/infraction state is STILL written to the DB (userReputationsTable) for
enforcement — only the LLM context injection is removed, so moderation
actions (mute/ban via infraction thresholds) keep working.
Net: even smaller prompts (no per-user context at all) → more messages fit
per request, and one fewer DB round-trip per unique user per sub-batch.
tsc, biome, vitest (129) all clean.
User insight: personal profile summaries bloat the prompt (less room per
request) and add a per-user DB/Redis round-trip for little moderation signal.
Only the behavioural <user_reputation> history is kept.
- textBatchProcessor: stop fetching getUserProfile; remove <user_profiles>
block + <user_profile_ref> from message tags. Keep <user_reputation>.
- mediaBatchProcessor + visionAnalyzer: same removal (profile fetch + ref).
- prompts/system.ts + prompts/output.ts: drop stale <user_profiles>/
<user_profile_ref> instructions; point LLM at <user_reputation> instead.
- aiAnalyzer: gate userProfileLearner behind AI_USER_PROFILE_LEARNING_ENABLED
(default false) — generates profiles nobody reads, pure LLM/DB waste.
- Add AI_USER_PROFILE_LEARNING_ENABLED config knob.
Net: smaller prompts (more messages fit per request), fewer DB round-trips
per sub-batch, and no background LLM calls learning unused profiles.
tsc, biome, vitest (129) all clean.
User insight: rather than many small per-batch API requests, pack many
messages into ONE request so a burst is analyzed with far fewer calls.
- AI_LLM_TEXT_BATCH_SIZE 20 -> 60 (one request now carries ~3x more messages).
- AI_ANALYSIS_MAX_TARGET_TOKENS 4000 -> 14000 (the scheduler's token-budget
gate was trimming pending messages to ~20 before they reached the sub-batch
splitter; raising it lets ~60 messages through to a single LLM call).
- AI_LLM_TEXT_ANALYSIS_TIMEOUT_MS 30000 -> 45000 (one larger call needs more
headroom; gemini-flash-lite has a 1M-token context so 14k+8k is trivial).
Net effect when ramai: a 60-message burst = 1-2 API calls instead of 3+,
less semaphore contention, faster throughput.
- Parallelize per-user reputation/profile fetches in textBatchProcessor
(was a serial ~2N DB/Redis round-trip loop per sub-batch; now Promise.all
over unique users). Cuts per-batch latency, biggest win on small/quiet
batches.
- Make the LLM concurrency semaphore dynamic (cached per config value) instead
of frozen at import time, so AI_LLM_MAX_CONCURRENT is tunable without code
change and reflects current config.
- Bump AI_LLM_MAX_CONCURRENT default 5 -> 8 (gemini-flash-lite is cheap; helps
throughput when busy).
- Lower AI_ANALYSIS_DEBOUNCE_MS 500 -> 250 (snappier first-message analysis
when quiet).
- Lower AI_ANALYSIS_RECOVERY_INTERVAL_MS 15000 -> 10000 (stuck/errored
messages re-analyze sooner).
tsc, biome, vitest (129) all clean.
Backend returns messages DESC (newest first); the view previously rendered
that directly, so the feed was inverted vs Discord (old at bottom, new at top)
while the load-older control sat at the top — contradictory.
- Reverse the display list so it reads oldest→newest top→bottom, like DC.
- Load-older (cursor to lower created_at) prepends at the top; scroll position
is preserved by offsetting scrollTop by the height added above.
- Open at the bottom (newest visible) on first load / scope change.
- New live messages append at the bottom and auto-scroll only when the user is
already near the bottom (nearBottomRef), so reading history isn't disrupted.
- Scroll container now tracked via ref; onScroll updates nearBottom + triggers
load-older when scrolled to the top.
tsc, biome, next build all clean.
- Set stream:false on the /chat/completions request so the bot gets one
complete response instead of an SSE token stream.
- Add reasoning_effort:"none" to suppress extended-thinking/reasoning tokens
(ignored by non-reasoning models like gemini-flash-lite).
- Add parseResponse(): handles both the JSON object 9router returns for
stream:false and the SSE text it may still emit, delegating SSE to parseSse.
Verified live: omniroute returns 200 application/json with message.content.
- Add viewport export with viewportFit: "cover" so iOS exposes
env(safe-area-inset-*) (required for the insets to take effect).
- NavRail / TopBar / main / Toaster now respect safe-area insets so content
clears the iPhone notch and home indicator in both portrait and landscape.
- prefers-reduced-motion: the media query already disabled declared animation
classes; harden it with a global transition/animation duration override and
kill the scan-line shimmer so motion-sensitive users get a fully static UI.
Verified tsc --noEmit + next build clean.
- SectionHeader: action (filters/legends) now wraps below the title on narrow
screens instead of overflowing beside it (flex-wrap, gap-2 sm:gap-3).
- GuildChannelPicker: selects go full-width and stack on mobile (w-full
sm:w-44 / sm:w-52) instead of fixed widths that exceeded a 375px viewport.
- Messages search: w-full sm:w-64 so it doesn't crowd the picker on mobile.
- TopBar: tighter padding (px-4 sm:px-5), smaller title on mobile, connection
status uses compact (dot only) on mobile, ambient pill hidden < sm.
- Shell main + dashboard channel label: responsive padding / shrink-0 widths.
Verified tsc --noEmit + next build clean; targets breakpoints 375/768/1024/1440.
- Show an explicit Loader2 spinner row ("Loading older…") while the next page
fetches, instead of a disabled button.
- Cap appended older pages at MAX_OLDER_PAGES=10 (500 messages) so a long
scroll-up never pulls the entire history; show a "capped" hint pointing to
search. Reset the counter when guild/channel changes.
Wire the existing useLoadMore + useMessagesHasMore pagination hooks into the
Messages view: add a "↑ Load older messages" button at the top of the list and
auto-load the next (older) page when the user scrolls to the top. Backend
messages.list already returns a created_at-based nextCursor (DESC order), so
older pages are just subsequent cursors. Newest-first live feed is preserved;
the load-older control is hidden during search.
Browser connects oRPC over wss://…/trpc (partysocket). The gmw-proxy nginx
only forwarded /api and /ws to the backend, so /trpc upgrades fell through to
Next.js SSR and the socket never opened ("WebSocket is not open"). Add a
/trpc location (WS upgrade headers) mirroring /ws. Backend already serves
oRPC on /trpc (HTTP RPCHandler + WS ORPCWebSocketServer on :4001).
Verified: ws://127.0.0.1:4001/trpc upgrade OPEN; SSR + server-side fetch RPCLink
also use /trpc directly so only the browser path was broken.
flake.nix only rewrote @/ aliases but left extensionless relative imports
(./router) in compiled dist/. node dist/index.js (how prod runs) cannot
resolve extensionless ESM specifiers -> ERR_MODULE_NOT_FOUND -> backend
crashlooped (444 restarts, port 4001 dead). Extract the fixer into a shared
scripts/fix-imports.mjs that appends .js to extensionless relative imports and
rewrites @/ aliases, and wire it into backend + discord-gateway build phases.
Verified: fresh tsc + fixer -> node dist/index.js boots; oRPC over /trpc
serves both HTTP POST and WebSocket (config/dashboard/voice/moderation/
media/chatbot/analysis) end-to-end against Postgres + Redis. next build
passes with the oRPC client + partysocket.
Replace REST module routers with a single typed tRPC appRouter served over
/trpc (HTTP + WebSocket), and rewire the frontend to call it via
@trpc/client wsLink (browser) and httpLink (RSC data layer). Existing
/api/health + /api/metrics stay as plain Express for infra scraping.
Notable fixes surfaced by the live smoke test:
- Express 5 / path-to-regexp v8 rejects the /trpc/* wildcard route; use a
prefix middleware that computes opts.path from the URL instead.
- nodeHTTPRequestHandler treats opts.path as the literal procedure path, so
it is derived per-request from req.url.
- Two ws servers on one http.Server (the /ws voice socket + /trpc) collided
and returned 400 on upgrade; both now use noServer + a manually routed
server.on('upgrade') keyed by path.
Verified: BE tsc+biome+40 vitest green; FE tsc+biome green; live
HTTP and WebSocket calls returned real prod data.
Co-Authored-By: Claude Opus 4.5 (1M context) <noreply@anthropic.com>