Skip to content

feat(redis): Comprehensive Redis integration (#177)#220

Merged
BillChirico merged 25 commits intomainfrom
feat/issue-177
Mar 2, 2026
Merged

feat(redis): Comprehensive Redis integration (#177)#220
BillChirico merged 25 commits intomainfrom
feat/issue-177

Conversation

@BillChirico
Copy link
Collaborator

Summary

Completes feature #177 — comprehensive Redis integration for caching, distributed rate limiting, and graceful degradation.

What's already implemented (prior commits on this branch)

  • Centralized Redis client (src/redis.js) with singleton pattern, graceful degradation when Redis is unavailable, auto-reconnect with backoff, and metrics tracking (hit rate, connections)
  • Unified cache utility (src/utils/cache.js) — Redis-backed with in-memory LRU fallback; transparent to callers
  • Discord API response caching (src/utils/discordCache.js) — fetchChannelCached, fetchGuildChannelsCached, fetchGuildRolesCached, fetchMemberCached with appropriate TTLs
  • Reputation data caching (src/utils/reputationCache.js) — getReputationCached, getLeaderboardCached, getRankCached with invalidation hooks
  • Distributed rate limiter (src/api/middleware/redisRateLimit.js) — Redis sliding window with in-memory fallback
  • Health endpoint integration — Redis stats (hit rate, memory, connection status) surfaced in /api/v1/health
  • Config invalidation (src/config-listeners.js) — cache eviction on config changes via DB LISTEN/NOTIFY

What this PR adds (final wiring)

  • server.js + community.js: Switch from in-memory rateLimit to redisRateLimit for distributed rate limiting
  • /rank command: Uses getReputationCached + setReputationCache + getRankCached — avoids redundant DB queries
  • /leaderboard command: Uses getLeaderboardCached — TTL-cached query
  • Module coverage: fetchChannelCached now wired in commands/welcome.js, commands/review.js, modules/welcomeOnboarding.js, modules/reviewHandler.js
  • Test fixes: Proper mocks for cache/discordCache in affected test files; restore ioredis constructor mock syntax

Test results

2874 passing, 6 failing (all pre-existing triage.coverage.test.js failures unrelated to this feature).

Acceptance criteria

  • Redis client connected on startup with graceful degradation
  • All listed Discord API fetches use cache
  • Config caching reduces DB queries (in-memory config cache + Redis overlay)
  • Reputation queries cached with proper invalidation
  • Leaderboard loads in <100ms (cached)
  • Distributed rate limiting via Redis
  • Metrics/logging for cache hit/miss rates via health endpoint

Phase 1 of #177: Core infrastructure

- src/redis.js: Centralized Redis client with graceful degradation,
  reconnect strategy, connection stats, and hit/miss tracking
- src/utils/cache.js: High-level cache helpers (get/set/del/pattern/getOrSet)
  with Redis backend and in-memory LRU fallback
- Configurable TTLs via environment variables
- Non-blocking SCAN for pattern deletes
- Update index.js to initialize Redis on startup and close on shutdown
- Add stopCacheCleanup() to graceful shutdown sequence
- Update redisClient.js to be a thin re-export from centralized redis.js
- Add Redis stats (connected, hit/miss, hitRate) to /health endpoint

Part of #177
- discordCache.js: Cached fetchers for channels, roles, members, guild channels
  with TTL-based expiration and cache invalidation helpers
- reputationCache.js: XP/level/leaderboard/rank caching with proper
  invalidation on XP gain events

Part of #177
- Replace client.channels.fetch() with fetchChannelCached() in 12 modules:
  scheduler, reminderHandler, pollHandler, challengeScheduler, githubFeed,
  moderation, rateLimit, linkFilter, reviewHandler, starboard, welcome,
  triage-respond
- Add leaderboard DB query caching in community.js (TTL 5min)
- Add reputation cache invalidation on XP gain in reputation.js
- All fetches use Redis when available, in-memory LRU fallback otherwise

Part of #177
- Add cache invalidation listeners in config-listeners.js for welcome,
  starboard, and reputation config changes
- Add comprehensive tests: redis.test.js (8 tests), cache.test.js (20 tests),
  discordCache.test.js (10 tests), reputationCache.test.js (6 tests)
- Update 16 existing test files with discordCache mock to prevent
  import resolution errors from fetchChannelCached
- Fix community.test.js to mock cacheGetOrSet pass-through
- Update redisClient.coverage.test.js for US spelling
- Update config-listeners.test.js for new listener count (8→11)
- Update welcome.coverage.test.js for graceful null return behavior

All 2869 passing tests pass. 6 pre-existing failures in triage.coverage.test.js
(unrelated to this PR).

Part of #177
- redisRateLimit.js: Sliding window rate limiting using Redis INCR/PTTL
  with automatic fallback to in-memory when Redis is unavailable
- Atomic multi/exec pipeline for consistent counting
- Same API contract as existing rateLimit.js (drop-in replacement)
- 5 tests covering Redis path, fallback, 429 response, error recovery

Part of #177
- Add Redis service to railway.toml (free plan)
- Add Redis container to docker-compose.yml with healthcheck
- Add REDIS_URL env vars to bot and web services
- Add redisdata volume for persistence
multi().exec() returns [[err, value], ...] tuples. Now destructures
and checks each command's error before using its value; falls back
to in-memory rate limiter on pipeline failure.

Also removes the unused windowSec variable.
Only cacheGetOrSet and TTL are used; cacheGet and cacheSet were dead imports.
reputationCache was deleting the base key 'leaderboard:${guildId}'
but the paginated API caches under 'leaderboard:${guildId}:${page}:${limit}'.
Now uses cacheDelPattern('leaderboard:${guildId}:*') consistently so
all paginated leaderboard entries are properly invalidated.
An import statement had leaked into the JSDoc block, creating invalid
comment syntax. Cleaned up the header to be proper JSDoc.
Cache was being invalidated BEFORE the level update write, allowing
stale data to be re-cached in the gap. Now invalidates AFTER all
reputation writes (XP upsert + level update) are complete.
The in-memory fallback for cacheDelPattern converted glob patterns
to regex without escaping special regex chars like '.', '+', etc.
Now escapes all metacharacters before substituting '*' and '?'.
flushdb() wipes ALL Redis keys — dangerous in shared environments.
Now scans and deletes only known app-prefixed keys (rl:*, reputation:*,
rank:*, leaderboard:*, discord:*, config:*, session:*).
If client was non-null when _resetRedis() was called, the existing
connection was silently abandoned. Now calls client.quit() (with
disconnect() fallback) before resetting state.
Channel metadata was stored in Redis but never returned when the
DJS cache missed — the code only re-checked DJS. Now returns the
cached {id, name, type, guildId} object directly, avoiding an
unnecessary Discord API call.
cacheDelPattern was dynamically imported via await import('./cache.js')
but cache.js is already statically imported at the top. Added it to
the existing static import instead.
If an assertion failed before vi.useRealTimers() was called, fake
timers would leak into subsequent tests. Wrapped in try/finally
to ensure timers are always restored.
Tests were using the old base key 'leaderboard:${guildId}' format.
Updated to use paginated keys matching the new pattern-based invalidation.
…urn type

- config-listeners: change leaderboard invalidation pattern from
  `leaderboard:${guildId}:*` to `leaderboard:${guildId}*` so it
  matches the actual stored key format (no colon suffix)

- discordCache: fetchChannelCached no longer returns a plain metadata
  object when a Redis cache hit is found. Cached data is used only for
  the existence recheck; function always falls through to Discord API
  fetch so callers receive a real Discord.js Channel (or null)

Fixes review threads PRRT_kwDORICdSM5xceQX, PRRT_kwDORICdSM5xchrZ
_resetRedisClient() was calling the async _resetRedis() without await,
meaning callers had no way to wait for the reset to complete, leading to
potential in-flight connection races in tests and shutdown sequences.

Fixes review thread PRRT_kwDORICdSM5xchrL
…tener keys

redis.test.js:
- Make beforeEach/afterEach async and await _resetRedis() to prevent
  cross-test flakiness from in-flight connections

config-listeners.test.js:
- Add explicit toContain assertions for welcome.*, starboard.*, and
  reputation.* so the test will fail if a cache-invalidation listener
  is removed

Fixes review threads PRRT_kwDORICdSM5xchrQ, PRRT_kwDORICdSM5xchrU
- Switch server.js and community.js from in-memory rateLimit to redisRateLimit
  for distributed rate limiting across bot instances
- Wire /rank command to use getReputationCached + setReputationCache + getRankCached,
  avoiding redundant DB queries on hot command paths
- Wire /leaderboard command to use getLeaderboardCached (TTL cached)
- Extend fetchChannelCached to commands/welcome.js, commands/review.js,
  modules/welcomeOnboarding.js, and modules/reviewHandler.js
- Update tests: mock reputationCache in leaderboard/rank tests to isolate
  cache layer; mock discordCache in review/welcomeOnboarding tests;
  restore ioredis constructor mock syntax broken by biome formatter

Closes #177
Copilot AI review requested due to automatic review settings March 2, 2026 04:54
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 2, 2026

Warning

Rate limit exceeded

@BillChirico has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 2 minutes and 28 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📥 Commits

Reviewing files that changed from the base of the PR and between a03bf04 and e0c1bf4.

📒 Files selected for processing (2)
  • src/api/server.js
  • src/index.js
📝 Walkthrough

Walkthrough

This PR introduces a comprehensive caching and Redis-backed rate limiting infrastructure across the application. It replaces in-process rate limiting with Redis-backed rate limiting, adds caching layers for leaderboard, rank, and channel lookups, and updates cache invalidation patterns and test mocks accordingly.

Changes

Cohort / File(s) Summary
Rate Limiting Migration to Redis
src/api/routes/community.js, src/api/server.js
Switched from in-process rate limiter to Redis-backed rate limiter, updating imports, type annotations, and runtime instantiation with explicit key namespace prefix.
Leaderboard and Rank Caching
src/commands/leaderboard.js, src/commands/rank.js
Added caching layers wrapping database queries for leaderboard retrieval and reputation/rank lookups, with cache-first fetch patterns and fallback to DB queries on cache miss.
Channel Caching Integration
src/commands/review.js, src/commands/welcome.js, src/modules/reviewHandler.js, src/modules/triage-respond.js, src/modules/welcomeOnboarding.js
Replaced direct Discord API channel fetches with cached fetch helper (fetchChannelCached), with graceful fallbacks to preserve existing error handling semantics.
Cache Utilities and Configuration
src/utils/discordCache.js, src/utils/reputationCache.js, src/config-listeners.js
Updated cache utility behavior: channel cache hits now fall through to API fetch, removed redundant cleanup calls, and refined cache invalidation patterns for guild-scoped keys.
Test Mocks and Expectations
tests/commands/leaderboard.test.js, tests/commands/rank.test.js, tests/commands/review.test.js, tests/config-listeners.test.js, tests/modules/welcomeOnboarding.test.js
Added mocks for new caching utilities (getLeaderboardCached, reputationCache module, discordCache) and updated cache invalidation test expectations to match refined pattern.

Possibly related issues

Possibly related PRs

Suggested reviewers

  • claude
🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 64.29% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main change: comprehensive Redis integration for caching, distributed rate limiting, and graceful degradation.
Description check ✅ Passed The description is detailed and directly related to the changeset, explaining prior implementations, what this PR adds, test results, and completed acceptance criteria.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/issue-177

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@greptile-apps
Copy link

greptile-apps bot commented Mar 2, 2026

Greptile Summary

Wires Redis caching and distributed rate limiting into API routes, commands, and modules. The Redis integration itself is well-implemented with proper fallback mechanisms, but merge conflict resolution accidentally removed multiple recently-merged features.

What works well:

  • Distributed rate limiting via redisRateLimit with graceful fallback to in-memory
  • Cache-aside pattern for reputation queries, leaderboards, and Discord API calls
  • Proper Redis initialization in startup and shutdown sequences
  • Test mocks properly configured for new cache dependencies

CRITICAL issues blocking merge:

Action required:
Restore all accidentally removed features before merging. The Redis integration changes themselves are solid and should be preserved.

Confidence Score: 0/5

  • This PR cannot be safely merged due to accidental removal of multiple recently-added features
  • While the Redis integration code is well-implemented, merge conflict resolution in src/index.js and src/modules/triage-respond.js removed 6+ recently-merged features (command aliases, performance monitoring, bot status, backups, temp roles, AI feedback, protected target checks, webhooks). These modules still exist but are no longer initialized, which will break functionality for users depending on these features.
  • Pay critical attention to src/index.js and src/modules/triage-respond.js — both files have significant unintentional deletions from merge conflict resolution

Important Files Changed

Filename Overview
src/index.js CRITICAL: Merge conflict resolution removed multiple features (aliases, perf monitoring, bot status, backups, temp roles, webhooks)
src/modules/triage-respond.js CRITICAL: Removed AI feedback reactions, protected target checks, and webhook notifications
src/api/server.js Switched to redisRateLimit for distributed rate limiting with proper fallback
src/api/routes/community.js Switched to redisRateLimit with appropriate keyPrefix for community endpoints
src/commands/rank.js Added reputation and rank caching with proper cache-aside pattern and fallback to DB
src/commands/leaderboard.js Added leaderboard caching using getLeaderboardCached wrapper

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    Start[API Request / Command Execution] --> RateLimit{Redis Available?}
    RateLimit -->|Yes| RedisRL[Redis Rate Limiter<br/>Sliding Window]
    RateLimit -->|No| MemoryRL[In-Memory Fallback]
    
    RedisRL --> CheckLimit{Under Limit?}
    MemoryRL --> CheckLimit
    CheckLimit -->|No| Reject[429 Too Many Requests]
    CheckLimit -->|Yes| Handler[Request Handler]
    
    Handler --> NeedData{Need Data?}
    NeedData -->|Reputation| RepCache{Cache Hit?}
    NeedData -->|Leaderboard| LbCache{Cache Hit?}
    NeedData -->|Discord API| DiscCache{Cache Hit?}
    
    RepCache -->|Hit| ReturnCache1[Return Cached]
    RepCache -->|Miss| QueryDB1[Query PostgreSQL]
    QueryDB1 --> CacheDB1[Cache Result]
    CacheDB1 --> ReturnCache1
    
    LbCache -->|Hit| ReturnCache2[Return Cached]
    LbCache -->|Miss| QueryDB2[Query PostgreSQL]
    QueryDB2 --> CacheDB2[Cache Result]
    CacheDB2 --> ReturnCache2
    
    DiscCache -->|Hit| ReturnCache3[Fetch from Discord API]
    DiscCache -->|Miss| QueryAPI[Call Discord API]
    QueryAPI --> CacheAPI[Cache Metadata]
    CacheAPI --> ReturnCache3
    
    ReturnCache1 --> Response[Generate Response]
    ReturnCache2 --> Response
    ReturnCache3 --> Response
    Response --> End[Return to User]
Loading

Last reviewed commit: e0c1bf4

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/commands/leaderboard.js`:
- Around line 34-44: The leaderboard code uses interaction.guildId directly as a
cache key and SQL filter which can be undefined; add a guild-context guard at
the start of the command handler to abort early (e.g., send an ephemeral error
like "This command must be used in a server") and do not call
getLeaderboardCached or pool.query when interaction.guildId is falsy. Locate the
call to getLeaderboardCached and the SQL query (references:
getLeaderboardCached, pool.query and the SQL selecting from reputation) and wrap
them behind a check for interaction.guildId (or throw/return before reaching
them) so you never create or read from a "leaderboard:undefined" cache bucket.

In `@src/modules/triage-respond.js`:
- Around line 354-355: The code currently swallows all exceptions from
fetchChannelCached(client, channelId) by using .catch(() => null); change this
to a try/catch so you can log unexpected errors while still falling back to
null: call fetchChannelCached(client, channelId) inside a try block, assign the
result to channel, and in catch inspect the error and call the existing logger
(e.g., processLogger or logger) to record the error with context (include
channelId and client/guild context) before setting channel = null so guildId =
channel?.guildId continues to work; keep any expected-not-found handling as a
null fallback but do not silence other exceptions.

In `@src/utils/discordCache.js`:
- Around line 37-40: The debug message currently hardcodes "Redis cache hit..."
which is misleading when the metadata may come from an in-memory fallback;
update the debug(...) call that logs "Redis cache hit for channel metadata;
fetching real channel from API" (the debug invocation that uses channelId) to
use neutral wording such as "cache hit for channel metadata; fetching real
channel from API" or include the actual source if available (e.g., add a source
variable like cacheSource and log it along with channelId) so logs no longer
misattribute hits to Redis.

In `@tests/commands/rank.test.js`:
- Around line 26-30: Add a new test in tests/commands/rank.test.js that
simulates a cache hit by having the mocked getReputationCached return a
reputation row (instead of null) and call the /rank command handler in
src/commands/rank.js; assert that the code path that queries the database for
reputation is not invoked (i.e., the repository/DB method used by rank.js to
fetch reputation is not called) and that the response uses the cached value—use
the existing mocks getReputationCached, getRankCached, and setReputationCache to
set up and verify the behavior.

In `@tests/modules/welcomeOnboarding.test.js`:
- Line 16: The current expression chains .catch() directly after an optional
call to client?.channels?.fetch?.(channelId), which can be undefined and cause a
TypeError; update the mock so you first obtain the result of
client?.channels?.fetch?.(channelId) into a variable (or use an additional
optional check) and only call .catch() if that result is a Promise, e.g. get
const result = client?.channels?.fetch?.(channelId) and return result ?
result.catch(() => null) : Promise.resolve(null); ensure you reference the
existing client, channels, fetch and channelId symbols when applying the change.

ℹ️ Review info

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between bcf04e2 and a03bf04.

📒 Files selected for processing (17)
  • src/api/routes/community.js
  • src/api/server.js
  • src/commands/leaderboard.js
  • src/commands/rank.js
  • src/commands/review.js
  • src/commands/welcome.js
  • src/config-listeners.js
  • src/modules/reviewHandler.js
  • src/modules/triage-respond.js
  • src/modules/welcomeOnboarding.js
  • src/utils/discordCache.js
  • src/utils/reputationCache.js
  • tests/commands/leaderboard.test.js
  • tests/commands/rank.test.js
  • tests/commands/review.test.js
  • tests/config-listeners.test.js
  • tests/modules/welcomeOnboarding.test.js
💤 Files with no reviewable changes (1)
  • src/utils/reputationCache.js
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Greptile Review
  • GitHub Check: Docker Build Validation
🧰 Additional context used
📓 Path-based instructions (6)
**/*.js

📄 CodeRabbit inference engine (AGENTS.md)

**/*.js: Use ESM modules — use import/export, never require()
Always use node: protocol for Node.js builtins (e.g. import { readFileSync } from 'node:fs')
Always use semicolons in JavaScript code
Use single quotes for strings (enforced by Biome)
Use 2-space indentation (enforced by Biome)

Files:

  • tests/commands/leaderboard.test.js
  • src/modules/triage-respond.js
  • src/commands/welcome.js
  • src/commands/review.js
  • src/modules/reviewHandler.js
  • tests/commands/review.test.js
  • src/modules/welcomeOnboarding.js
  • src/commands/leaderboard.js
  • src/api/routes/community.js
  • tests/commands/rank.test.js
  • src/config-listeners.js
  • tests/modules/welcomeOnboarding.test.js
  • src/commands/rank.js
  • tests/config-listeners.test.js
  • src/utils/discordCache.js
  • src/api/server.js
tests/**/*.js

📄 CodeRabbit inference engine (AGENTS.md)

All new code must include tests. Test coverage must maintain an 80% threshold on statements, branches, functions, and lines. Run pnpm test before every commit

Files:

  • tests/commands/leaderboard.test.js
  • tests/commands/review.test.js
  • tests/commands/rank.test.js
  • tests/modules/welcomeOnboarding.test.js
  • tests/config-listeners.test.js
src/**/*.js

📄 CodeRabbit inference engine (AGENTS.md)

src/**/*.js: Always use Winston logger — import { info, warn, error } from '../logger.js'. NEVER use console.log, console.warn, console.error, or any console.* method in src/ files — replace any existing console calls with Winston equivalents
Pass structured metadata to Winston logs: info('Message processed', { userId, channelId })

Files:

  • src/modules/triage-respond.js
  • src/commands/welcome.js
  • src/commands/review.js
  • src/modules/reviewHandler.js
  • src/modules/welcomeOnboarding.js
  • src/commands/leaderboard.js
  • src/api/routes/community.js
  • src/config-listeners.js
  • src/commands/rank.js
  • src/utils/discordCache.js
  • src/api/server.js
src/modules/*.js

📄 CodeRabbit inference engine (AGENTS.md)

Per-request modules (AI, spam, moderation) should call getConfig(guildId) on every invocation for automatic config changes. Stateful resources should use onConfigChange listeners for reactive updates

Files:

  • src/modules/triage-respond.js
  • src/modules/reviewHandler.js
  • src/modules/welcomeOnboarding.js
src/commands/*.js

📄 CodeRabbit inference engine (AGENTS.md)

src/commands/*.js: Slash commands must export data (SlashCommandBuilder) and execute(interaction) function. Export adminOnly = true for mod-only commands
Moderation commands must follow the shared pattern: deferReply({ ephemeral: true }), validate inputs, sendDmNotification(), execute Discord action, createCase(), sendModLogEmbed(), checkEscalation()

Files:

  • src/commands/welcome.js
  • src/commands/review.js
  • src/commands/leaderboard.js
  • src/commands/rank.js
src/api/routes/*.js

📄 CodeRabbit inference engine (AGENTS.md)

Always use custom error classes from src/utils/errors.js and log errors with context before re-throwing

Files:

  • src/api/routes/community.js
🧠 Learnings (3)
📚 Learning: 2026-03-01T06:03:34.399Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-01T06:03:34.399Z
Learning: Applies to src/commands/*.js : Moderation commands must follow the shared pattern: `deferReply({ ephemeral: true })`, validate inputs, `sendDmNotification()`, execute Discord action, `createCase()`, `sendModLogEmbed()`, `checkEscalation()`

Applied to files:

  • src/commands/welcome.js
  • src/commands/review.js
  • src/modules/welcomeOnboarding.js
  • src/commands/leaderboard.js
📚 Learning: 2026-03-01T06:03:34.399Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-01T06:03:34.399Z
Learning: Applies to src/commands/*.js : Slash commands must export `data` (SlashCommandBuilder) and `execute(interaction)` function. Export `adminOnly = true` for mod-only commands

Applied to files:

  • src/commands/review.js
  • src/commands/leaderboard.js
  • src/commands/rank.js
📚 Learning: 2026-03-01T06:03:34.399Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-01T06:03:34.399Z
Learning: Applies to src/modules/*.js : Per-request modules (AI, spam, moderation) should call `getConfig(guildId)` on every invocation for automatic config changes. Stateful resources should use `onConfigChange` listeners for reactive updates

Applied to files:

  • src/config-listeners.js
🧬 Code graph analysis (12)
src/modules/triage-respond.js (1)
src/utils/discordCache.js (2)
  • channel (45-45)
  • fetchChannelCached (22-65)
src/commands/welcome.js (1)
src/utils/discordCache.js (1)
  • fetchChannelCached (22-65)
src/commands/review.js (3)
src/utils/discordCache.js (1)
  • fetchChannelCached (22-65)
tests/commands/review.test.js (1)
  • interaction (200-224)
src/modules/reviewHandler.js (1)
  • reviewChannelId (302-302)
src/modules/reviewHandler.js (1)
src/utils/discordCache.js (2)
  • channel (45-45)
  • fetchChannelCached (22-65)
src/modules/welcomeOnboarding.js (1)
src/utils/discordCache.js (1)
  • fetchChannelCached (22-65)
src/commands/leaderboard.js (1)
src/utils/reputationCache.js (1)
  • getLeaderboardCached (59-61)
src/api/routes/community.js (1)
src/api/middleware/redisRateLimit.js (1)
  • redisRateLimit (22-78)
src/config-listeners.js (1)
src/utils/cache.js (1)
  • cacheDelPattern (178-211)
tests/modules/welcomeOnboarding.test.js (1)
src/modules/welcomeOnboarding.js (1)
  • introChannel (177-177)
src/commands/rank.js (2)
src/utils/reputationCache.js (3)
  • getReputationCached (18-21)
  • setReputationCache (31-35)
  • getRankCached (71-73)
src/commands/leaderboard.js (2)
  • pool (33-33)
  • rows (34-44)
src/utils/discordCache.js (2)
src/utils/cache.js (1)
  • cacheGet (76-115)
src/logger.js (1)
  • debug (224-226)
src/api/server.js (2)
src/api/middleware/redisRateLimit.js (1)
  • redisRateLimit (22-78)
tests/api/middleware/redisRateLimit.test.js (1)
  • redisRateLimit (27-27)
🔇 Additional comments (13)
src/api/server.js (1)

9-9: LGTM!

The Redis rate limiter integration is correctly wired:

  • Import path is valid
  • Type annotation properly updated
  • Default configuration (15min window, 100 requests, rl prefix) is reasonable for the global API limiter
  • Existing cleanup logic via destroy() remains compatible with the new middleware

Also applies to: 18-19, 65-66

src/api/routes/community.js (1)

15-15: LGTM!

The Redis rate limiter configuration is well-designed:

  • Aggressive 30 req/min limit is appropriate for public, unauthenticated endpoints
  • keyPrefix: 'rl:community' correctly namespaces these rate limit keys separately from the global rl prefix in server.js, preventing cross-contamination between global and route-specific limits
  • The limiter is properly applied to all routes via router.use()

Also applies to: 20-25

src/config-listeners.js (1)

106-109: Scoped leaderboard cache invalidation looks correct.

The move to leaderboard:${guildId}:* tightens invalidation to per-guild leaderboard keys and avoids broader unintended matches.

tests/config-listeners.test.js (1)

449-450: Test expectation update is aligned with runtime behavior.

This assertion now correctly tracks the narrowed leaderboard invalidation pattern.

tests/commands/leaderboard.test.js (1)

17-19: Cache mock strategy is solid for deterministic command tests.

Executing the provided factory directly is a clean way to bypass cache state in this suite.

src/commands/review.js (1)

163-171: Good fallback-preserving migration to fetchChannelCached.

This keeps the command resilient: it prefers configured channel resolution and cleanly falls back to the interaction channel when unavailable.

src/commands/welcome.js (1)

40-61: Channel fetch helper wiring is correct and safely guarded.

Using fetchChannelCached here is a good fit, and the isTextBased checks still prevent invalid send targets.

tests/commands/review.test.js (1)

30-38: Mock coverage for discordCache integration is well structured.

This prevents unrelated cache-layer behavior from destabilizing /review command tests while still exercising the channel-resolution path.

src/modules/welcomeOnboarding.js (2)

9-9: Good cache-aware channel resolution integration.

Using fetchChannelCached here keeps channel lookup behavior consistent with the new shared Discord cache path and preserves existing null/text-channel safety checks.

Also applies to: 177-177


122-122: Role resolution fallback looks solid.

The cache-first role lookup with API fetch-on-miss is concise and safe for onboarding role assignment flows.

tests/modules/welcomeOnboarding.test.js (1)

95-100: Nice fixture update for interaction.client.channels.

This aligns the test interaction shape with the new production lookup path (fetchChannelCached(interaction.client, ...)).

src/modules/reviewHandler.js (1)

13-13: Good consolidation on shared channel caching helper.

Switching both call sites to fetchChannelCached improves consistency and keeps the same graceful-failure behavior.

Also applies to: 127-127, 307-307

src/commands/rank.js (1)

14-18: Cache-first reputation/rank integration is implemented cleanly.

The fallback flow (cache → DB → cache write) and rank cache wrapper are wired correctly while preserving command response behavior.

Also applies to: 51-63, 78-87

Copilot AI review requested due to automatic review settings March 2, 2026 09:36
coderabbitai[bot]
coderabbitai bot previously approved these changes Mar 2, 2026
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 17 out of 17 changed files in this pull request and generated 4 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

coderabbitai[bot]
coderabbitai bot previously approved these changes Mar 2, 2026
Copilot AI review requested due to automatic review settings March 2, 2026 12:16
@BillChirico BillChirico merged commit 83c0cec into main Mar 2, 2026
8 of 13 checks passed
@BillChirico BillChirico deleted the feat/issue-177 branch March 2, 2026 12:17
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 14 out of 14 changed files in this pull request and generated 7 comments.

Comments suppressed due to low confidence (1)

src/index.js:227

  • This change removes the alias → canonical command resolution flow (and the corresponding permission lookup), which effectively disables per-guild command aliases while keeping the rest of the command execution pipeline intact. If command aliases are still a supported feature, restore alias resolution (and permission checks against the resolved target), otherwise consider removing the alias feature end-to-end (incl. DB load / docs) in a separate PR to avoid an unexpected breaking behavior change in a Redis-focused PR.
    // Permission check
    const guildConfig = getConfig(interaction.guildId);
    if (!hasPermission(member, commandName, guildConfig)) {
      const permLevel = guildConfig.permissions?.allowedCommands?.[commandName] || 'administrator';
      await safeReply(interaction, {
        content: getPermissionError(commandName, permLevel),
        ephemeral: true,
      });

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.


// Execute command from collection
const command = client.commands.get(resolvedCommandName);
const command = client.commands.get(commandName);
Copy link

Copilot AI Mar 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change removes the alias → canonical command resolution flow (and the corresponding permission lookup), which effectively disables per-guild command aliases while keeping the rest of the command execution pipeline intact. If command aliases are still a supported feature, restore alias resolution (and permission checks against the resolved target), otherwise consider removing the alias feature end-to-end (incl. DB load / docs) in a separate PR to avoid an unexpected breaking behavior change in a Redis-focused PR.

Copilot uses AI. Check for mistakes.
@@ -139,31 +106,6 @@ export async function sendModerationLog(client, classification, snapshot, channe
// Find target messages from the snapshot
const targets = snapshot.filter((m) => classification.targetMessageIds?.includes(m.messageId));

Copy link

Copilot AI Mar 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The protected-target gating logic was removed from sendModerationLog, so moderation logs will now be generated even when the target is an admin/mod/owner (previously explicitly skipped). If that protection is still a requirement, reintroduce the check (ideally still allowing an explicit config override) so sensitive moderation actions aren’t logged for protected roles.

Suggested change
// Skip logging when any protected target (admin/mod/owner/staff) is involved,
// unless explicitly overridden via config.triage.logProtectedTargets === true.
const allowProtectedLogs = config.triage?.logProtectedTargets === true;
const hasProtectedTarget = targets.some(
(t) => t?.isProtected || t?.isStaff || t?.isMod || t?.isAdmin || t?.isOwner,
);
if (!allowProtectedLogs && hasProtectedTarget) {
return;
}

Copilot uses AI. Check for mistakes.

try {
const channel = await client.channels.fetch(review.channel_id).catch(() => null);
const channel = await fetchChannelCached(client, review.channel_id);
Copy link

Copilot AI Mar 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Error-handling for fetchChannelCached is inconsistent in this PR (e.g., triage-respond wraps it with .catch(() => null) but this call site does not). Either standardize on catching/recovering at each call site or guarantee (and document) that fetchChannelCached never rejects so callers can safely await it without try/catch.

Suggested change
const channel = await fetchChannelCached(client, review.channel_id);
const channel = await fetchChannelCached(client, review.channel_id).catch(() => null);

Copilot uses AI. Check for mistakes.
Comment on lines +51 to +63
// Fetch reputation row (cached)
const cachedRep = await getReputationCached(interaction.guildId, target.id);
let repRow = cachedRep;
if (!repRow) {
const { rows } = await pool.query(
'SELECT xp, level, messages_count FROM reputation WHERE guild_id = $1 AND user_id = $2',
[interaction.guildId, target.id],
);
repRow = rows[0] ?? null;
if (repRow) {
await setReputationCache(interaction.guildId, target.id, repRow);
}
}
Copy link

Copilot AI Mar 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new cache branches (rep cache hit vs miss; rank cache factory invocation) aren’t directly asserted in tests. Add coverage in tests/commands/rank.test.js to validate: (1) on cache hit, pool.query is not called and setReputationCache is not called; (2) on cache miss, pool.query is called and setReputationCache is called with the fetched row; (3) getRankCached bypasses the DB query when returning a cached value.

Copilot uses AI. Check for mistakes.
Comment on lines +78 to +87
// Rank position in guild (cached)
const rank = await getRankCached(interaction.guildId, target.id, async () => {
const rankRow = await pool.query(
`SELECT COUNT(*) + 1 AS rank
FROM reputation
WHERE guild_id = $1 AND xp > $2`,
[interaction.guildId, xp],
);
return { rank: Number(rankRow.rows[0]?.rank ?? 1) };
}).then((r) => r?.rank ?? 1);
Copy link

Copilot AI Mar 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new cache branches (rep cache hit vs miss; rank cache factory invocation) aren’t directly asserted in tests. Add coverage in tests/commands/rank.test.js to validate: (1) on cache hit, pool.query is not called and setReputationCache is not called; (2) on cache miss, pool.query is called and setReputationCache is called with the fetched row; (3) getRankCached bypasses the DB query when returning a cached value.

Copilot uses AI. Check for mistakes.
Comment on lines +79 to +87
const rank = await getRankCached(interaction.guildId, target.id, async () => {
const rankRow = await pool.query(
`SELECT COUNT(*) + 1 AS rank
FROM reputation
WHERE guild_id = $1 AND xp > $2`,
[interaction.guildId, xp],
);
return { rank: Number(rankRow.rows[0]?.rank ?? 1) };
}).then((r) => r?.rank ?? 1);
Copy link

Copilot AI Mar 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mixing await with a chained .then(...) makes the flow harder to read and slightly harder to debug. Prefer awaiting the cached result into a variable and then deriving rank from that value in a separate statement.

Copilot uses AI. Check for mistakes.
Comment on lines +34 to +44
const rows = await getLeaderboardCached(interaction.guildId, async () => {
const result = await pool.query(
`SELECT user_id, xp, level
FROM reputation
WHERE guild_id = $1
ORDER BY xp DESC
LIMIT 10`,
[interaction.guildId],
);
return result.rows;
});
Copy link

Copilot AI Mar 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add/update tests in tests/commands/leaderboard.test.js to assert the caching wrapper is used (e.g., getLeaderboardCached called with the guild id) and to validate behavior on cache hit vs miss (DB query executed only when the cache factory runs). This ensures the new integration actually prevents redundant DB calls.

Copilot uses AI. Check for mistakes.
Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

14 files reviewed, 4 comments

Edit Code Review Agent Settings | Greptile

Comment on lines 43 to +56
startConversationCleanup,
stopConversationCleanup,
} from './modules/ai.js';
import { startScheduledBackups, stopScheduledBackups } from './modules/backup.js';
import { startBotStatus, stopBotStatus } from './modules/botStatus.js';
import { loadAliasesFromDb, resolveAlias } from './modules/commandAliases.js';
import { getConfig, loadConfig } from './modules/config.js';
import { registerEventHandlers } from './modules/events.js';
import { startGithubFeed, stopGithubFeed } from './modules/githubFeed.js';
import { checkMem0Health, markUnavailable } from './modules/memory.js';
import { startTempbanScheduler, stopTempbanScheduler } from './modules/moderation.js';
import { loadOptOuts } from './modules/optout.js';
import { PerformanceMonitor } from './modules/performanceMonitor.js';
import { startScheduler, stopScheduler } from './modules/scheduler.js';
import { startTempRoleScheduler, stopTempRoleScheduler } from './modules/tempRoleHandler.js';
import { startTriage, stopTriage } from './modules/triage.js';
import { startVoiceFlush, stopVoiceFlush } from './modules/voice.js';
import { fireEventAllGuilds } from './modules/webhookNotifier.js';
import { closeRedisClient as closeRedis, initRedis } from './redis.js';
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CRITICAL: Merge conflict resolution removed imports for recently-merged features (backup, botStatus, commandAliases, performanceMonitor, tempRoleHandler). These modules still exist in the codebase but are no longer being initialized.

The commit message "fix: resolve merge conflicts with main" suggests this was unintentional. These features need to be restored:

Suggested change
startConversationCleanup,
stopConversationCleanup,
} from './modules/ai.js';
import { startScheduledBackups, stopScheduledBackups } from './modules/backup.js';
import { startBotStatus, stopBotStatus } from './modules/botStatus.js';
import { loadAliasesFromDb, resolveAlias } from './modules/commandAliases.js';
import { getConfig, loadConfig } from './modules/config.js';
import { registerEventHandlers } from './modules/events.js';
import { startGithubFeed, stopGithubFeed } from './modules/githubFeed.js';
import { checkMem0Health, markUnavailable } from './modules/memory.js';
import { startTempbanScheduler, stopTempbanScheduler } from './modules/moderation.js';
import { loadOptOuts } from './modules/optout.js';
import { PerformanceMonitor } from './modules/performanceMonitor.js';
import { startScheduler, stopScheduler } from './modules/scheduler.js';
import { startTempRoleScheduler, stopTempRoleScheduler } from './modules/tempRoleHandler.js';
import { startTriage, stopTriage } from './modules/triage.js';
import { startVoiceFlush, stopVoiceFlush } from './modules/voice.js';
import { fireEventAllGuilds } from './modules/webhookNotifier.js';
import { closeRedisClient as closeRedis, initRedis } from './redis.js';
} from './modules/ai.js';
import { startScheduledBackups, stopScheduledBackups } from './modules/backup.js';
import { startBotStatus, stopBotStatus } from './modules/botStatus.js';
import { loadAliasesFromDb, resolveAlias } from './modules/commandAliases.js';
import { getConfig, loadConfig } from './modules/config.js';
import { registerEventHandlers } from './modules/events.js';
import { startGithubFeed, stopGithubFeed } from './modules/githubFeed.js';
import { checkMem0Health, markUnavailable } from './modules/memory.js';
import { startTempbanScheduler, stopTempbanScheduler } from './modules/moderation.js';
import { loadOptOuts } from './modules/optout.js';
import { PerformanceMonitor } from './modules/performanceMonitor.js';
import { startScheduler, stopScheduler } from './modules/scheduler.js';
import { startTempRoleScheduler, stopTempRoleScheduler } from './modules/tempRoleHandler.js';
import { startTriage, stopTriage } from './modules/triage.js';
import { startVoiceFlush, stopVoiceFlush } from './modules/voice.js';
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/index.js
Line: 43-56

Comment:
CRITICAL: Merge conflict resolution removed imports for recently-merged features (backup, botStatus, commandAliases, performanceMonitor, tempRoleHandler). These modules still exist in the codebase but are no longer being initialized.

The commit message "fix: resolve merge conflicts with main" suggests this was unintentional. These features need to be restored:
- Command aliases (#166/#203)
- Performance monitoring (#218)
- Bot status/activity (#40/#210)
- Server backups (#209)
- Temp role assignment (#128/#208)

```suggestion
} from './modules/ai.js';
import { startScheduledBackups, stopScheduledBackups } from './modules/backup.js';
import { startBotStatus, stopBotStatus } from './modules/botStatus.js';
import { loadAliasesFromDb, resolveAlias } from './modules/commandAliases.js';
import { getConfig, loadConfig } from './modules/config.js';
import { registerEventHandlers } from './modules/events.js';
import { startGithubFeed, stopGithubFeed } from './modules/githubFeed.js';
import { checkMem0Health, markUnavailable } from './modules/memory.js';
import { startTempbanScheduler, stopTempbanScheduler } from './modules/moderation.js';
import { loadOptOuts } from './modules/optout.js';
import { PerformanceMonitor } from './modules/performanceMonitor.js';
import { startScheduler, stopScheduler } from './modules/scheduler.js';
import { startTempRoleScheduler, stopTempRoleScheduler } from './modules/tempRoleHandler.js';
import { startTriage, stopTriage } from './modules/triage.js';
import { startVoiceFlush, stopVoiceFlush } from './modules/voice.js';
```

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines 217 to 230
@@ -270,7 +230,7 @@ client.on('interactionCreate', async (interaction) => {
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Command alias resolution was removed but the commandAliases module still exists. Aliases allow per-guild custom command names and should be restored.

Suggested change
try {
info('Slash command received', { command: commandName, user: interaction.user.tag });
// Resolve alias → target command (per-guild custom aliases).
// Do this early so permission checks and command lookup both use the
// resolved (canonical) command name rather than the alias name.
const resolvedCommandName = resolveAlias(interaction.guildId, commandName) || commandName;
// Permission check (using resolved command name so alias permissions mirror target)
const guildConfig = getConfig(interaction.guildId);
if (!hasPermission(member, resolvedCommandName, guildConfig)) {
const permLevel =
guildConfig.permissions?.allowedCommands?.[resolvedCommandName] || 'administrator';
await safeReply(interaction, {
content: getPermissionError(commandName, permLevel),
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/index.js
Line: 217-230

Comment:
Command alias resolution was removed but the `commandAliases` module still exists. Aliases allow per-guild custom command names and should be restored.

```suggestion
  try {
    info('Slash command received', { command: commandName, user: interaction.user.tag });

    // Resolve alias → target command (per-guild custom aliases).
    // Do this early so permission checks and command lookup both use the
    // resolved (canonical) command name rather than the alias name.
    const resolvedCommandName = resolveAlias(interaction.guildId, commandName) || commandName;

    // Permission check (using resolved command name so alias permissions mirror target)
    const guildConfig = getConfig(interaction.guildId);
    if (!hasPermission(member, resolvedCommandName, guildConfig)) {
      const permLevel =
        guildConfig.permissions?.allowedCommands?.[resolvedCommandName] || 'administrator';
      await safeReply(interaction, {
        content: getPermissionError(commandName, permLevel),
```

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines 282 to 286
stopTriage();
stopConversationCleanup();
stopTempbanScheduler();
stopTempRoleScheduler();
stopScheduler();
stopGithubFeed();
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing shutdown calls for removed features (tempRoleScheduler, scheduledBackups, botStatus, voiceFlush, perfMonitor).

Suggested change
stopTriage();
stopConversationCleanup();
stopTempbanScheduler();
stopTempRoleScheduler();
stopScheduler();
stopGithubFeed();
stopTriage();
stopConversationCleanup();
stopTempbanScheduler();
stopTempRoleScheduler();
stopScheduler();
stopGithubFeed();
stopScheduledBackups();
perfMonitor.stop();
stopBotStatus();
stopVoiceFlush();
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/index.js
Line: 282-286

Comment:
Missing shutdown calls for removed features (tempRoleScheduler, scheduledBackups, botStatus, voiceFlush, perfMonitor).

```suggestion
  stopTriage();
  stopConversationCleanup();
  stopTempbanScheduler();
  stopTempRoleScheduler();
  stopScheduler();
  stopGithubFeed();
  stopScheduledBackups();
  perfMonitor.stop();
  stopBotStatus();
  stopVoiceFlush();
```

How can I resolve this? If you propose a fix, please make it concise.

@greptile-apps
Copy link

greptile-apps bot commented Mar 2, 2026

Additional Comments (1)

src/modules/triage-respond.js
Removed imports for AI feedback reactions and protected target checks. These features (aiFeedback module for 👍/👎 reactions, isProtectedTarget to prevent moderating admins/mods) were present in the base branch and should not be removed as part of Redis integration.

import { EmbedBuilder } from 'discord.js';
import { info, error as logError, warn } from '../logger.js';
import { buildDebugEmbed, extractStats, logAiUsage } from '../utils/debugFooter.js';
import { fetchChannelCached } from '../utils/discordCache.js';
import { safeSend } from '../utils/safeSend.js';
import { splitMessage } from '../utils/splitMessage.js';
import { addToHistory } from './ai.js';
import { FEEDBACK_EMOJI, registerAiMessage } from './aiFeedback.js';
import { isProtectedTarget } from './moderation.js';
import { resolveMessageId, sanitizeText } from './triage-filter.js';
import { fireEvent } from './webhookNotifier.js';
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/modules/triage-respond.js
Line: 6-13

Comment:
Removed imports for AI feedback reactions and protected target checks. These features (aiFeedback module for 👍/👎 reactions, isProtectedTarget to prevent moderating admins/mods) were present in the base branch and should not be removed as part of Redis integration.

```suggestion
import { EmbedBuilder } from 'discord.js';
import { info, error as logError, warn } from '../logger.js';
import { buildDebugEmbed, extractStats, logAiUsage } from '../utils/debugFooter.js';
import { fetchChannelCached } from '../utils/discordCache.js';
import { safeSend } from '../utils/safeSend.js';
import { splitMessage } from '../utils/splitMessage.js';
import { addToHistory } from './ai.js';
import { FEEDBACK_EMOJI, registerAiMessage } from './aiFeedback.js';
import { isProtectedTarget } from './moderation.js';
import { resolveMessageId, sanitizeText } from './triage-filter.js';
import { fireEvent } from './webhookNotifier.js';
```

How can I resolve this? If you propose a fix, please make it concise.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants