Conversation
📝 WalkthroughSummary by CodeRabbit
WalkthroughTwo new utility modules are introduced: Changes
Sequence Diagram(s)sequenceDiagram
actor Client
participant withRetry
participant ErrorClassifier
participant Operation
participant Backoff
Client->>withRetry: call withRetry(fn, options)
loop Retry Loop (up to maxRetries)
withRetry->>Operation: execute operation
alt Operation Succeeds
Operation-->>withRetry: return result
withRetry-->>Client: return success
else Operation Fails
Operation-->>withRetry: throw error
withRetry->>ErrorClassifier: classifyError(error)
ErrorClassifier-->>withRetry: ErrorType
withRetry->>ErrorClassifier: isRetryable(error)
ErrorClassifier-->>withRetry: boolean
alt Retryable & Retries Remaining
withRetry->>Backoff: calculateBackoff(attempt)
Backoff-->>withRetry: delay time
withRetry->>withRetry: sleep(delay)
Note over withRetry: Log retry attempt, loop continues
else Non-Retryable or No Retries Left
withRetry-->>Client: throw error
end
end
end
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes 🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
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. Comment |
- Add errors.js with error classification, user-friendly messages, and retry detection - Add retry.js with exponential backoff for transient failures - Integrate with existing Winston logger from main branch
005cfd4 to
c2bfc4a
Compare
There was a problem hiding this comment.
Actionable comments posted: 4
🤖 Fix all issues with AI agents
In `@src/utils/errors.js`:
- Around line 91-94: The current detection only inspects the error message
string (`message`) to map to ErrorType.CONFIG_MISSING; update the logic in the
function in src/utils/errors.js to also check the error object's `code` property
(e.g., `code === 'ENOENT'`) and handle cases where the input may be an Error
object rather than a string, so that when either message includes 'config.json
not found' or `code === 'ENOENT'` you return ErrorType.CONFIG_MISSING; reference
the existing `message` variable and the `ErrorType.CONFIG_MISSING` symbol when
making this change.
- Around line 114-146: The messages map inside getUserFriendlyMessage is
recreated on every call; move the messages object to module scope (e.g., const
MESSAGES = { ... }) and have getUserFriendlyMessage reference
MESSAGES[errorType]. Update references to ErrorType keys accordingly and keep
the default fallback to MESSAGES[ErrorType.UNKNOWN] so behavior is unchanged
while avoiding repeated allocations.
- Around line 51-56: The ETIMEDOUT branch is currently classified as NETWORK and
thus never reaches the TIMEOUT check; update the logic in the function that maps
error codes (uses the variables code, message and the ErrorType enum) so
ETIMEDOUT maps to ErrorType.TIMEOUT: remove ETIMEDOUT from the first conditional
(the one returning ErrorType.NETWORK) or explicitly check ETIMEDOUT before the
NETWORK branch, and ensure the second conditional (checking ETIMEDOUT or
message.includes('timeout')) returns ErrorType.TIMEOUT.
In `@src/utils/retry.js`:
- Around line 27-33: The calculateBackoff function uses pure exponential backoff
which risks synchronized retries; modify calculateBackoff(attempt, baseDelay,
maxDelay) to add randomized jitter to the computed delay (for example by
applying a random factor or selecting a random value within a range around the
exponential value) and still cap the result at maxDelay: compute the exponential
delay as now, apply jitter via Math.random to spread retries, then return
Math.min(jitteredDelay, maxDelay). Ensure the jitter keeps delays non-negative
and documented in the function comment.
📜 Review details
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
📒 Files selected for processing (2)
src/utils/errors.jssrc/utils/retry.js
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-11-26T01:57:34.920Z
Learnt from: CR
Repo: VolvoxCommunity/Volvox.Website PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-26T01:57:34.920Z
Learning: Applies to src/**/*.{ts,tsx} : Use `reportError(context, error)` from `src/lib/logger.ts` to report errors to Sentry with context metadata, falling back to console.error if Sentry is disabled
Applied to files:
src/utils/errors.js
🧬 Code graph analysis (2)
src/utils/retry.js (2)
src/utils/errors.js (5)
isRetryable(192-204)errorType(115-115)errorType(156-156)errorType(193-193)classifyError(43-105)src/logger.js (3)
warn(225-227)error(232-234)debug(211-213)
src/utils/errors.js (2)
src/logger.js (1)
error(232-234)src/utils/retry.js (2)
errorType(67-67)isRetryable(49-55)
⏰ 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). (1)
- GitHub Check: Cursor Bugbot
🔇 Additional comments (7)
src/utils/errors.js (3)
11-34: LGTM!The
ErrorTypeenum is well-organized with clear categorization of error types (network, API, Discord-specific, configuration, and unknown). Good practice to use string values for easier debugging and logging.
155-183: LGTM!The function appropriately returns
nullfor error types where no actionable suggestion exists (API_ERROR, UNKNOWN). The documented return type correctly indicates this behavior.
192-204: LGTM!Good selection of retryable error types. Rate limits are correctly included since they're transient. The actual backoff handling in
retry.jswill manage the delay appropriately.src/utils/retry.js (4)
8-9: LGTM!Imports are appropriate - using the error classification utilities from the sibling module and logging functions from the logger.
16-18: LGTM!Standard sleep implementation using Promise and setTimeout.
48-118: LGTM!The retry logic is well-structured:
- Correctly handles the initial attempt plus retries
- Logs appropriately at different levels (warn on first failure, debug for retry attempts, error on final failure)
- Properly integrates with the error classification system
- Clean separation of retryable vs non-retryable error handling
126-129: LGTM!Clean factory pattern allowing pre-configured retry wrappers. Option merging order correctly allows per-call overrides.
✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.
| if (code === 'ECONNREFUSED' || code === 'ENOTFOUND' || code === 'ETIMEDOUT') { | ||
| return ErrorType.NETWORK; | ||
| } | ||
| if (code === 'ETIMEDOUT' || message.includes('timeout')) { | ||
| return ErrorType.TIMEOUT; | ||
| } |
There was a problem hiding this comment.
ETIMEDOUT classification is unreachable for TIMEOUT.
ETIMEDOUT is checked at line 51 and returns NETWORK, so the ETIMEDOUT check at line 54 for TIMEOUT will never execute. Since ETIMEDOUT specifically indicates a connection timeout, it should likely return TIMEOUT, not NETWORK.
🐛 Proposed fix
// Network errors
- if (code === 'ECONNREFUSED' || code === 'ENOTFOUND' || code === 'ETIMEDOUT') {
+ if (code === 'ECONNREFUSED' || code === 'ENOTFOUND') {
return ErrorType.NETWORK;
}
if (code === 'ETIMEDOUT' || message.includes('timeout')) {
return ErrorType.TIMEOUT;
}🤖 Prompt for AI Agents
In `@src/utils/errors.js` around lines 51 - 56, The ETIMEDOUT branch is currently
classified as NETWORK and thus never reaches the TIMEOUT check; update the logic
in the function that maps error codes (uses the variables code, message and the
ErrorType enum) so ETIMEDOUT maps to ErrorType.TIMEOUT: remove ETIMEDOUT from
the first conditional (the one returning ErrorType.NETWORK) or explicitly check
ETIMEDOUT before the NETWORK branch, and ensure the second conditional (checking
ETIMEDOUT or message.includes('timeout')) returns ErrorType.TIMEOUT.
| // Config errors | ||
| if (message.includes('config.json not found') || message.includes('enoent')) { | ||
| return ErrorType.CONFIG_MISSING; | ||
| } |
There was a problem hiding this comment.
ENOENT detection may miss file-not-found errors.
Node.js filesystem errors set error.code = 'ENOENT', but this code only checks the message. Consider also checking the code variable.
🛠️ Proposed fix
// Config errors
- if (message.includes('config.json not found') || message.includes('enoent')) {
+ if (message.includes('config.json not found') || message.includes('enoent') || code === 'ENOENT') {
return ErrorType.CONFIG_MISSING;
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // Config errors | |
| if (message.includes('config.json not found') || message.includes('enoent')) { | |
| return ErrorType.CONFIG_MISSING; | |
| } | |
| // Config errors | |
| if (message.includes('config.json not found') || message.includes('enoent') || code === 'ENOENT') { | |
| return ErrorType.CONFIG_MISSING; | |
| } |
🤖 Prompt for AI Agents
In `@src/utils/errors.js` around lines 91 - 94, The current detection only
inspects the error message string (`message`) to map to
ErrorType.CONFIG_MISSING; update the logic in the function in
src/utils/errors.js to also check the error object's `code` property (e.g.,
`code === 'ENOENT'`) and handle cases where the input may be an Error object
rather than a string, so that when either message includes 'config.json not
found' or `code === 'ENOENT'` you return ErrorType.CONFIG_MISSING; reference the
existing `message` variable and the `ErrorType.CONFIG_MISSING` symbol when
making this change.
| export function getUserFriendlyMessage(error, context = {}) { | ||
| const errorType = classifyError(error, context); | ||
|
|
||
| const messages = { | ||
| [ErrorType.NETWORK]: "I'm having trouble connecting to my brain right now. Check if the AI service is running and try again!", | ||
|
|
||
| [ErrorType.TIMEOUT]: "That took too long to process. Try again with a shorter message, or wait a moment and retry!", | ||
|
|
||
| [ErrorType.API_RATE_LIMIT]: "Whoa, too many requests! Let's take a quick breather. Try again in a minute.", | ||
|
|
||
| [ErrorType.API_UNAUTHORIZED]: "I'm having authentication issues with the AI service. An admin needs to check the API credentials.", | ||
|
|
||
| [ErrorType.API_NOT_FOUND]: "The AI service endpoint isn't responding. Please check if it's configured correctly.", | ||
|
|
||
| [ErrorType.API_SERVER_ERROR]: "The AI service is having technical difficulties. It should recover automatically - try again in a moment!", | ||
|
|
||
| [ErrorType.API_ERROR]: "Something went wrong with the AI service. Give it another shot in a moment!", | ||
|
|
||
| [ErrorType.DISCORD_PERMISSION]: "I don't have permission to do that! An admin needs to check my role permissions.", | ||
|
|
||
| [ErrorType.DISCORD_CHANNEL_NOT_FOUND]: "I can't find that channel. It might have been deleted, or I don't have access to it.", | ||
|
|
||
| [ErrorType.DISCORD_MISSING_ACCESS]: "I don't have access to that resource. Please check my permissions!", | ||
|
|
||
| [ErrorType.CONFIG_MISSING]: "Configuration file not found! Please create a config.json file (you can copy from config.example.json).", | ||
|
|
||
| [ErrorType.CONFIG_INVALID]: "The configuration file has errors. Please check config.json for syntax errors or missing required fields.", | ||
|
|
||
| [ErrorType.UNKNOWN]: "Something unexpected happened. Try again, and if it keeps happening, check the logs for details.", | ||
| }; | ||
|
|
||
| return messages[errorType] || messages[ErrorType.UNKNOWN]; | ||
| } |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Consider moving the messages object outside the function.
The messages object is recreated on every call. Moving it to module scope would avoid repeated object creation, though the impact is minor.
♻️ Proposed refactor
+const USER_FRIENDLY_MESSAGES = {
+ [ErrorType.NETWORK]: "I'm having trouble connecting to my brain right now. Check if the AI service is running and try again!",
+ [ErrorType.TIMEOUT]: "That took too long to process. Try again with a shorter message, or wait a moment and retry!",
+ // ... remaining messages
+ [ErrorType.UNKNOWN]: "Something unexpected happened. Try again, and if it keeps happening, check the logs for details.",
+};
+
export function getUserFriendlyMessage(error, context = {}) {
const errorType = classifyError(error, context);
- const messages = {
- // ... all messages
- };
- return messages[errorType] || messages[ErrorType.UNKNOWN];
+ return USER_FRIENDLY_MESSAGES[errorType] || USER_FRIENDLY_MESSAGES[ErrorType.UNKNOWN];
}🤖 Prompt for AI Agents
In `@src/utils/errors.js` around lines 114 - 146, The messages map inside
getUserFriendlyMessage is recreated on every call; move the messages object to
module scope (e.g., const MESSAGES = { ... }) and have getUserFriendlyMessage
reference MESSAGES[errorType]. Update references to ErrorType keys accordingly
and keep the default fallback to MESSAGES[ErrorType.UNKNOWN] so behavior is
unchanged while avoiding repeated allocations.
| function calculateBackoff(attempt, baseDelay, maxDelay) { | ||
| // Exponential backoff: baseDelay * 2^attempt | ||
| const delay = baseDelay * Math.pow(2, attempt); | ||
|
|
||
| // Cap at maxDelay | ||
| return Math.min(delay, maxDelay); | ||
| } |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Consider adding jitter to prevent thundering herd.
Pure exponential backoff without jitter can cause synchronized retry storms when multiple operations fail simultaneously. Adding randomized jitter spreads out retries.
♻️ Proposed fix with jitter
function calculateBackoff(attempt, baseDelay, maxDelay) {
// Exponential backoff: baseDelay * 2^attempt
const delay = baseDelay * Math.pow(2, attempt);
- // Cap at maxDelay
- return Math.min(delay, maxDelay);
+ // Cap at maxDelay and add jitter (±25%)
+ const cappedDelay = Math.min(delay, maxDelay);
+ const jitter = cappedDelay * 0.25 * (Math.random() * 2 - 1);
+ return Math.max(0, Math.floor(cappedDelay + jitter));
}🤖 Prompt for AI Agents
In `@src/utils/retry.js` around lines 27 - 33, The calculateBackoff function uses
pure exponential backoff which risks synchronized retries; modify
calculateBackoff(attempt, baseDelay, maxDelay) to add randomized jitter to the
computed delay (for example by applying a random factor or selecting a random
value within a range around the exponential value) and still cap the result at
maxDelay: compute the exponential delay as now, apply jitter via Math.random to
spread retries, then return Math.min(jitteredDelay, maxDelay). Ensure the jitter
keeps delays non-negative and documented in the function comment.
| } | ||
| if (code === 'ETIMEDOUT' || message.includes('timeout')) { | ||
| return ErrorType.TIMEOUT; | ||
| } |
There was a problem hiding this comment.
ETIMEDOUT classified as NETWORK instead of TIMEOUT
Medium Severity
The error code ETIMEDOUT is checked in the network errors condition (line 51) before the timeout condition (line 54). When an error has code === 'ETIMEDOUT', it returns ErrorType.NETWORK instead of ErrorType.TIMEOUT. The second check for ETIMEDOUT on line 54 becomes unreachable for that code path. This causes timeout errors to show network-related user messages instead of timeout-specific guidance.
…cord values - getThreadConfig now validates reuseWindowMinutes (must be positive number) and autoArchiveMinutes (must be valid number), falling back to safe defaults - Add snapAutoArchiveDuration() that snaps to nearest Discord-allowed value (60, 1440, 4320, or 10080 minutes) - Invalid config values (NaN, negative, non-number) gracefully degrade Addresses PR #57 review threads #3 and #6.
…cord values - getThreadConfig now validates reuseWindowMinutes (must be positive number) and autoArchiveMinutes (must be valid number), falling back to safe defaults - Add snapAutoArchiveDuration() that snaps to nearest Discord-allowed value (60, 1440, 4320, or 10080 minutes) - Invalid config values (NaN, negative, non-number) gracefully degrade Addresses PR #57 review threads #3 and #6.
Add privacy documentation to the memory module explaining that user messages are sent to the mem0 hosted platform for processing. Update the /memory command description to indicate external storage. Users can view and delete their data via /memory view and /memory forget. Resolves review thread #6.
Add privacy documentation to the memory module explaining that user messages are sent to the mem0 hosted platform for processing. Update the /memory command description to indicate external storage. Users can view and delete their data via /memory view and /memory forget. Resolves review thread #6.
Add privacy documentation to the memory module explaining that user messages are sent to the mem0 hosted platform for processing. Update the /memory command description to indicate external storage. Users can view and delete their data via /memory view and /memory forget. Resolves review thread #6.
…uard - Add cursor-based pagination with `after` param to fetchUserGuilds, looping until all guilds are fetched (issue #2) - Propagate RefreshTokenError to session and auto-redirect to sign-in via SessionGuard component in Providers (issue #3) - Add dockerContext = ".." to railway.toml for monorepo Docker builds (issue #4) - Create /api/health returning 200 JSON; update railway.toml healthcheckPath (issue #5) - Conditionally render Add to Server buttons only when CLIENT_ID is set (issue #6) - Add AbortController cleanup to guild fetch useEffect in ServerSelector (issue #7) - Refuse unauthenticated bot API requests when BOT_API_SECRET is missing (issue #9) - Add retry + empty/error states to ServerSelector (issue #13 partial)
- Differentiate requireGuildAdmin (ADMINISTRATOR only) from requireGuildModerator (ADMINISTRATOR | MANAGE_GUILD), aligning REST admin check with slash-command isAdmin (#1, #2, #12) - Add botOwners startup warning when using default upstream ID (#3) - Add SESSION_SECRET, DISCORD_CLIENT_SECRET, DISCORD_REDIRECT_URI to README deployment table (#4) - Pass actual permission level to getPermissionError so modlog denial says 'moderator' not 'administrator' (#5) - Guard _seedOAuthState with NODE_ENV production check (#6) - Add test: valid JWT with no server-side session (#7) - Add DiscordApiError class with HTTP status (#8) - Add moderatorRoleId support to isModerator (#9) - Remove no-op delete override from SessionStore (#10) - Cap oauthStates at 10k entries (#11) - Fix hasOAuthGuildPermission docstring for bitwise OR semantics (#12) - Handle dashboard URL fragment collision (#13) - Cap guildCache at 10k entries (#14) - Add SESSION_TTL_MS co-location comment with JWT expiry (#15) - Cache SESSION_SECRET via lazy getter in verifyJwt (#16) - Remove PII (username) from OAuth auth log (#17)
- Differentiate requireGuildAdmin (ADMINISTRATOR only) from requireGuildModerator (ADMINISTRATOR | MANAGE_GUILD), aligning REST admin check with slash-command isAdmin (#1, #2, #12) - Add botOwners startup warning when using default upstream ID (#3) - Add SESSION_SECRET, DISCORD_CLIENT_SECRET, DISCORD_REDIRECT_URI to README deployment table (#4) - Pass actual permission level to getPermissionError so modlog denial says 'moderator' not 'administrator' (#5) - Guard _seedOAuthState with NODE_ENV production check (#6) - Add test: valid JWT with no server-side session (#7) - Add DiscordApiError class with HTTP status (#8) - Add moderatorRoleId support to isModerator (#9) - Remove no-op delete override from SessionStore (#10) - Cap oauthStates at 10k entries (#11) - Fix hasOAuthGuildPermission docstring for bitwise OR semantics (#12) - Handle dashboard URL fragment collision (#13) - Cap guildCache at 10k entries (#14) - Add SESSION_TTL_MS co-location comment with JWT expiry (#15) - Cache SESSION_SECRET via lazy getter in verifyJwt (#16) - Remove PII (username) from OAuth auth log (#17)
…utes (#6) Each route previously inlined its prefix string on every call. Extracts to module-scope const matching the pattern used by config/members/roles routes.
* feat(conversations): add flagged_messages migration * feat(conversations): add API endpoints for list, detail, search, flag, stats * feat(conversations): mount router in API index * feat(conversations): add conversation list and replay dashboard pages * feat(conversations): add Next.js API proxy routes * test(conversations): add API and grouping tests * fix(conversations): escape ILIKE wildcards to prevent wildcard injection * fix(conversations): remove unused _totalChars variable * fix(conversations): cap list query to 5000 rows to prevent memory exhaustion * fix(conversations): replace in-memory stats grouping with SQL aggregates * fix(conversations): bound conversation detail query by time window instead of full channel scan * style: alphabetize imports and format authorizeGuildAdmin calls * test(conversations): fix stats mock to match SQL conversation counting * test(conversations): add POST flag endpoint to guild validation test The auth test already covered all 5 endpoints including POST .../flag, but the guild-validation test only checked 4 GET endpoints, leaving the flag endpoint's guild validation untested. Resolves review thread PRRT_kwDORICdSM5xTeiw * fix(conversations): parameterize SQL interval and fix flag button a11y Thread 3: Replace string interpolation of CONVERSATION_GAP_MINUTES in the window-function SQL with a $2 parameter to avoid hardcoded literals. Passes CONVERSATION_GAP_MINUTES as a query value instead. Thread 4: Change flag button wrapper from `focus-within:opacity-100` to `group-focus-within:opacity-100` so the button becomes visible whenever any element in the message bubble group receives keyboard focus, not just when the button's own children are focused — matching the group-hover pattern and ensuring proper keyboard accessibility. Also: biome --write reformatted label.tsx and textarea.tsx (pre-existing style issues). * test: add ILIKE wildcard escape coverage for % and _ characters Adds test cases verifying that % and _ characters in conversation search queries are properly escaped before being used in ILIKE patterns, preventing them from acting as SQL wildcards. * fix: deterministic flag status for duplicate flagged_messages rows When a message has been flagged multiple times (flagged_messages has no UNIQUE constraint on message_id), the previous Map construction would silently overwrite entries in iteration order, making the displayed status non-deterministic. Order the SELECT by created_at DESC so the first row per message_id that lands in the Map is always the most recently created flag, giving a predictable 'latest wins' behaviour. * refactor: extract escapeIlike utility from logQuery inline impl Creates src/utils/escapeIlike.js as a shared, exported utility. Conversations route now imports it instead of duplicating the regex. * fix(conversations): use escapeIlike(), fix non-deterministic flag status, add 30-day default window, verify conversationId on flag POST - Import escapeIlike() instead of inline regex (DRY #4) - Default to last 30 days when no `from` filter to prevent unbounded LIMIT 5000 scan (#3) - Fix Map construction: iterate ORDER BY DESC rows and only set first occurrence per key so most-recent flag status wins (#1) - Verify flagged messageId belongs to the conversation's channel before inserting (#2) * test(conversations): add ILIKE backslash escape test and fix flag mocks for new anchor check - Add test for backslash (\) escaping in ILIKE search (#7) - Update 'flag a message' mock to include anchorCheck query result * refactor(web): add LOG_PREFIX constant to all 5 conversation proxy routes (#6) Each route previously inlined its prefix string on every call. Extracts to module-scope const matching the pattern used by config/members/roles routes. * fix(ui): use MessagesSquare icon for Conversations sidebar entry (#8) AI Chat already uses MessageSquare. Conversations now uses MessagesSquare to distinguish the two nav items visually. * fix(ui): show last 4 digits of channel snowflake instead of raw ID (#9) Raw Discord snowflakes are 18+ digit numbers. Showing `${channelId.slice(-4)}` gives a minimal but less jarring display until a proper channel name resolver is wired up. * refactor(ui): extract PAGE_SIZE constant in conversations page (#5) Replaces two hardcoded 25s with a single PAGE_SIZE = 25 constant. * docs(migration): explain missing FK on conversation_first_id (#10) conversation_first_id has no FK because conversations are not a separate table with their own PK. They are virtual groups derived from message rows. message_id already carries a FK for referential integrity. * fix(lint): suppress pre-existing biome a11y errors in label component * fix(conversations): stray $ in JSX channel display, increase query limit to 10k


Implement comprehensive error handling with user-friendly error messages, automatic retry for transient failures, and detailed error logging for debugging.