Skip to content

feat: AI conversation persistence with DB-backed history#18

Merged
BillChirico merged 24 commits intomainfrom
feat/conversation-persistence
Feb 11, 2026
Merged

feat: AI conversation persistence with DB-backed history#18
BillChirico merged 24 commits intomainfrom
feat/conversation-persistence

Conversation

@BillChirico
Copy link
Collaborator

@BillChirico BillChirico commented Feb 11, 2026

Summary

Move AI conversation history from in-memory Map to PostgreSQL with write-through caching. Conversations now survive restarts, support configurable history depth, and auto-clean old messages.

Changes

DB Schema (src/db.js)

  • New conversations table: id, channel_id, role, content, username, created_at
  • Composite index on (channel_id, created_at) for efficient lookups
  • Auto-created during initDb() alongside existing config table

AI Module (src/modules/ai.js) — +329 lines

  • Write-through: addToHistory() writes to both in-memory cache and DB simultaneously
  • Cache-first reads: getHistory() returns from cache; falls back to DB on cache miss
  • Startup hydration: loads last N messages per channel from DB into cache on boot
  • TTL cleanup: periodic deletion of messages older than ai.historyTTLDays (default 30)
  • Graceful fallback: works entirely in-memory if DATABASE_URL is not set — no crashes

Config (config.json)

  • ai.historyLength — max messages per channel (default: 20)
  • ai.historyTTLDays — auto-delete messages older than N days (default: 30)
  • Both configurable via /config set ai.historyLength 50 etc.

Startup/Shutdown (src/index.js)

  1. Init DB → load config → set AI pool reference → hydrate cache → start TTL scheduler
  2. Graceful shutdown stops cleanup scheduler before closing DB pool

Tests

  • 46 tests across tests/db.test.js and tests/modules/ai.test.js
  • All DB interactions mocked
  • Coverage: 89.7% statements, 81.8% branches, 86.3% functions, 90.5% lines

Files Changed

10 files changed, 1,140 insertions(+), 51 deletions(-)

Merge DB results with any messages added concurrently via addToHistory
instead of overwriting the cache array. DB messages come first, then
any new messages added since the cache miss, trimmed to configured limit.
@cursor

This comment has been minimized.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 11, 2026

Caution

Review failed

The pull request is closed.

📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Conversation history persisted to a database with startup hydration and write-through updates.
    • Periodic cleanup of old conversations and configurable history length/TTL.
    • New test watch script for faster development feedback.
  • Bug Fixes

    • Improved graceful shutdown to better track and finish in-flight requests.
    • Enhanced API error handling with safer fallbacks.
  • Chores

    • Added project lint/format config and updated formatting rules.
    • Test suite expanded and updated; minor dependency bump and .gitignore updated for coverage.

Walkthrough

Adds DB-backed persistent conversation history with in-memory caching, DB schema and cleanup jobs, startup/shutdown pool and AI integration, tooling/config updates (biome, gitignore, config fields), package script tweaks, event/JSDoc improvements, and expanded tests for DB and AI behavior.

Changes

Cohort / File(s) Summary
Repository config
/.gitignore, /biome.json, /config.json, /package.json
Added coverage/ to .gitignore; introduced biome.json lint/format config; added ai.historyLength and ai.historyTTLDays to config.json; added test:watch script and bumped dotenv version in package.json.
Database layer & tests
src/db.js, tests/db.test.js
Added DB schema creation for conversations table and two indexes, adjusted pool init guards and in-progress handling, and expanded tests to verify schema creation, concurrent init rejection, and pool/SSL behaviors.
AI module & tests
src/modules/ai.js, tests/modules/ai.test.js
Replaced pure in-memory history with hybrid in-memory + Postgres persistence, added DI pool getters/setters, async hydration (getHistoryAsync, initConversationHistory), write-through DB writes, TTL-driven cleanup (startConversationCleanup/stopConversationCleanup), and extensive test updates for hydration, trimming, and DB interactions.
Application lifecycle & tests
src/index.js, tests/index.test.js
Initialize DB pool at startup and pass to AI layer (setPool), hydrate conversation history from DB, start/stop cleanup scheduler, add request tracking (registerPendingRequest/removePendingRequest), and expose corresponding mocks in tests.
Event handling
src/modules/events.js
Switched to discord.js Events.* constants for registrations, updated JSDoc parameter types to Client, and improved unhandled-rejection logging fallback.
🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately and concisely describes the main change: moving AI conversation history to database-backed persistence with caching.
Description check ✅ Passed The description is comprehensive and directly related to the changeset, covering DB schema, AI module changes, config updates, startup/shutdown flow, and test coverage.
Docstring Coverage ✅ Passed Docstring coverage is 96.00% which is sufficient. The required threshold is 80.00%.

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

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/conversation-persistence

📜 Recent review details

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between eb513b3 and 252cd9d.

📒 Files selected for processing (2)
  • tests/db.test.js
  • tests/modules/ai.test.js

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.


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.

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: 10

🤖 Fix all issues with AI agents
In `@biome.json`:
- Around line 3-13: The includes array in biome.json currently lists only
specific files ("src/db.js","src/index.js","src/modules/ai.js") which leaves out
other source files like src/logger.js, src/modules/config.js,
src/modules/events.js, src/utils/*.js and src/commands/*.js; update the includes
to cover all source files (e.g., use "src/**/*.js") or explicitly add the
missing filenames/globs to the includes list, or if this narrow scope is
intentional add a TODO/comment indicating incremental rollout and which files
will be added later; ensure you update the includes array in biome.json (refer
to the "files" -> "includes" entry) accordingly.

In `@src/db.js`:
- Around line 99-113: The conversations table currently allows any text in the
role column; update the table DDL in src/db.js where the CREATE TABLE query for
conversations is executed to enforce allowed roles by adding a CHECK constraint
(e.g., CHECK (role IN ('user','assistant','system')) ) to the role column
definition, or if the table may already exist, add an ALTER TABLE ... ADD
CONSTRAINT check_role ... clause after the CREATE TABLE to add the CHECK
constraint; ensure the constraint name is unique (e.g.,
check_conversations_role) so the DB will reject invalid role values at the
schema level.

In `@src/index.js`:
- Around line 332-336: The file-based loadState() call is redundant because
initConversationHistory() immediately overwrites in-memory channel state from
DB; remove the loadState() invocation (and any file-only persistence hooks if no
longer needed) so the app relies solely on initConversationHistory() for startup
state, or alternatively modify initConversationHistory() to merge DB records
with existing in-memory state rather than overwriting — locate references to
loadState() and the initConversationHistory() function to either delete the
former and any dependent file persistence code, or implement a merge strategy
inside initConversationHistory() that preserves file-derived channels when DB
records are absent.

In `@src/modules/ai.js`:
- Around line 126-127: Both getHistory and getHistoryAsync are selecting
username in their SQL ("SELECT role, content, username") but the subsequent
mapping only uses role and content; remove the unused column to align with
initConversationHistory. Update the SQL in the getHistory and getHistoryAsync
functions to "SELECT role, content FROM conversations WHERE channel_id = $1" (or
equivalent) so only role and content are fetched, keeping the rest of the
function logic (the .map() that extracts role and content) unchanged.
- Around line 246-289: initConversationHistory currently performs an N+1 query
pattern by fetching distinct channel IDs then querying conversations per
channel; replace the per-channel loop with a single SQL query (using
ROW_NUMBER() PARTITION BY channel_id ORDER BY created_at DESC) executed via
pool.query to retrieve up to getHistoryLength() rows per channel in one
round-trip, then reconstruct the per-channel arrays and populate
conversationHistory (preserving the existing ordering and totalMessages
calculation); locate usages in initConversationHistory, conversationHistory.set,
getPool, getHistoryLength and the existing pool.query calls to implement this
single-query hydration.
- Around line 209-239: The DB write in addToHistory currently fire-and-forgets
and logs failures with logWarn; update the error handling in the
pool.query().catch block to use logError instead of logWarn, include full error
details (err and err.message) and relevant context (channelId, role, username)
so persistence failures are surfaced to alerting; optionally add a simple retry
attempt (e.g., one additional pool.query retry) guarded to avoid blocking the
function, but at minimum replace logWarn with logError in addToHistory and
enrich the logged context (references: addToHistory, conversationHistory,
getPool, pool.query, logWarn/logError).
- Around line 123-140: The inner const limit declared inside the .then()
callback shadows the outer const limit and should be removed; in the callback
that queries conversations (the function that builds dbHistory, current,
merged), delete the redundant "const limit = getHistoryLength()" and use the
previously declared outer limit variable when trimming merged history (e.g.,
when slicing/setting conversationHistory.get/channelId), ensuring
conversationHistory and any concurrent addToHistory logic continue to reference
the same limit value.
- Around line 116-154: getHistory currently sets and returns a new empty array
in conversationHistory but later replaces the map entry with a new array during
async DB hydration, leaving any existing references stale; change the hydration
path in getHistory so it mutates the existing array in-place (get current =
conversationHistory.get(channelId) or create it if missing, compute the merged
slice as now, then replace contents of the existing array via splice/push
instead of conversationHistory.set(... newArray)), ensuring callers holding the
original reference (e.g., code using getHistory, event handlers, addToHistory)
observe the hydrated messages; also keep the same logging (info/logWarn) and
preserve the trim-to-limit behavior used with getHistoryLength().

In `@tests/modules/ai.test.js`:
- Around line 219-285: Add a test exercising the race-condition fix in
initConversationHistory: mock the DB pool query so the first call returns
channel list and the second call returns a Promise you resolve later (capture
resolveDbQuery), call initConversationHistory(), call addToHistory('ch1',
'user', 'concurrent message') while hydration is pending, then resolve the DB
query with a row { role: 'user', content: 'from db' }, await the hydration
promise and assert via getHistory('ch1') that the DB message appears before the
concurrent message and that both messages are present (also assert trimming
behavior if limit applies); use setPool with a mockPool whose query is the
mockQuery and keep references to mockQuery/resolveDbQuery to orchestrate timing.
- Around line 479-493: The test title claims to verify the Authorization header
but only checks Content-Type; update the test for generateResponse to either be
renamed to match the current assertion or (preferred) set the token and assert
Authorization: set process.env.OPENCLAW_TOKEN = 'test-token' (or the actual
env/config key your code reads) before calling generateResponse('ch1', 'Hi',
'user', config), spy globalThis.fetch as done, then after the call assert
fetchCall[1].headers['Authorization'] === 'Bearer test-token' (and you can keep
the Content-Type assertion); also ensure to clean up/reset
process.env.OPENCLAW_TOKEN after the test.
📜 Review details

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a2153bc and 804d662.

📒 Files selected for processing (10)
  • .gitignore
  • biome.json
  • config.json
  • package.json
  • src/db.js
  • src/index.js
  • src/modules/ai.js
  • tests/db.test.js
  • tests/modules/ai.test.js
  • vitest.config.js
🧰 Additional context used
🧠 Learnings (3)
📚 Learning: 2025-10-10T15:05:26.145Z
Learnt from: CR
Repo: BillChirico/LUA-Obfuscator PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-10-10T15:05:26.145Z
Learning: Applies to package.json : Only add new packages when absolutely necessary or explicitly requested

Applied to files:

  • package.json
📚 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 **/*.{ts,tsx,js,jsx,json,css,md} : After changing or editing any files, run the complete validation workflow: `pnpm format && pnpm typecheck && pnpm lint && pnpm build` before committing

Applied to files:

  • package.json
  • biome.json
📚 Learning: 2026-02-04T02:20:09.131Z
Learnt from: CR
Repo: VolvoxCommunity/Volvox.Website PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-02-04T02:20:09.131Z
Learning: Applies to {package.json,.npmrc,pnpm-lock.yaml} : Use pnpm package manager (pinned to v10.23.0) with `.npmrc` configured for strict peer dependencies and disabled shamefully-hoist

Applied to files:

  • package.json
🧬 Code graph analysis (4)
src/db.js (1)
src/modules/ai.js (8)
  • pool (121-121)
  • pool (167-167)
  • pool (171-177)
  • pool (224-224)
  • pool (247-247)
  • pool (262-268)
  • pool (297-297)
  • pool (327-327)
src/index.js (3)
src/utils/registerCommands.js (1)
  • registerCommands (18-60)
src/db.js (3)
  • client (82-82)
  • closeDb (144-155)
  • initDb (54-127)
src/modules/ai.js (3)
  • stopConversationCleanup (314-320)
  • setPool (83-85)
  • initConversationHistory (246-289)
tests/db.test.js (1)
src/db.js (4)
  • initDb (54-127)
  • getPool (134-139)
  • closeDb (144-155)
  • pool (12-12)
src/modules/ai.js (3)
src/modules/config.js (5)
  • getConfig (122-124)
  • pool (58-58)
  • pool (72-72)
  • pool (158-158)
  • pool (228-228)
src/db.js (2)
  • getPool (134-139)
  • pool (12-12)
src/logger.js (1)
  • info (218-220)
⏰ 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 (14)
.gitignore (1)

20-22: LGTM!

Clean addition to ignore coverage output directory, consistent with the new vitest coverage configuration.

vitest.config.js (1)

1-19: LGTM!

Clean vitest configuration with sensible coverage thresholds and targeted coverage includes for the files changed in this PR.

tests/db.test.js (1)

1-147: LGTM!

Well-structured test suite with proper module isolation via vi.resetModules(), clean mock setup, and good coverage of the DB module's lifecycle (init, get, close) and schema verification.

src/index.js (1)

313-347: Startup sequence is well-ordered.

The flow (DB init → config load → pool wiring → file state load → DB hydration → cleanup scheduler) is logically sound. The file-loaded state acts as a fallback for channels without DB records, and DB hydration correctly overwrites on a per-channel basis.

src/modules/ai.js (2)

295-309: Cleanup scheduler captures pool reference at start time.

startConversationCleanup checks getPool() on Line 297, but runCleanup also calls getPool() on Line 327 at execution time. This is correctly implemented — if the pool becomes unavailable between scheduler start and a cleanup tick, the runCleanup call safely returns. Good defensive design.


358-429: History is appended after the API call, creating a brief inconsistency window.

Lines 418-419 add the user message and assistant reply to history only after a successful API response. If a second request for the same channel arrives while the first is in-flight, it won't see the first user message in context. For a Discord bot with typical traffic patterns this is largely benign, but worth documenting as a known trade-off.

Otherwise the response flow — async history load, prompt construction, fetch, error fallback — is clean.

package.json (1)

34-37: All package versions are valid and published on npm.

config.json (1)

7-9: LGTM!

Sensible defaults for conversation history management — 20 messages per channel and a 30-day TTL are reasonable for a Discord bot use case.

tests/modules/ai.test.js (6)

1-53: LGTM — clean test setup with proper isolation.

Mock hoisting, per-test state reset, and cleanup timer teardown in afterEach provide solid test hygiene.


84-158: Good coverage of addToHistory paths.

Trimming, DB write-through, null username, graceful fallback on DB failure, and custom config are all exercised.


160-217: LGTM — getHistoryAsync tests cover the critical paths well.

Cache-first retrieval, DB fallback with correct row reversal, empty results, DB errors, and no-pool scenarios are all verified.


287-345: LGTM — cleanup scheduler tests.

Good use of vi.waitFor to handle the fire-and-forget async cleanup invocation. Start/stop lifecycle, error handling, and safe idempotent stop are all covered.


368-477: LGTM — generateResponse tests are thorough.

Success/error/exception paths, health monitor integration, history updates with username propagation, and default config fallbacks are all well covered.

Also applies to: 495-518


521-546: LGTM — graceful fallback tests.

Testing in-memory-only operation and resilience when getConfig throws confirms the module degrades safely without a DB or config.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

- Replace specific file list with src/**/*.js glob for comprehensive coverage
- Replace console.log/error with structured logger calls across all source files
- Add node: protocol prefix to Node.js builtin imports
- Fix unused variables, import ordering, and formatting issues
- Apply useTemplate and useExponentiationOperator suggestions
Restrict role values to 'user', 'assistant', 'system' at the database
level to prevent invalid data from being persisted.
Document that loadState() is temporary migration scaffolding and
is effectively overwritten by initConversationHistory() when DB
persistence is available.
- Mutate existing channel history arrays in-place after async hydration to
  prevent stale references held by callers
- Remove shadowed limit variable in getHistory() hydration path
- Drop unused username columns from history SELECT queries
- Upgrade DB write-through failure logging to logError with channel/role/user
  context for persistence visibility
- Replace startup hydration N+1 query pattern with single ROW_NUMBER() query
- Add regression test for async hydration race where addToHistory occurs while
  DB hydration is pending; assert in-place array mutation and message ordering
- Update initConversationHistory tests for single ROW_NUMBER() hydration query
- Align query expectations with SELECT role, content
- Rename fetch header test to reflect actual assertion scope
coderabbitai[bot]
coderabbitai bot previously approved these changes Feb 11, 2026
@BillChirico
Copy link
Collaborator Author

Addressed all 10 unresolved review threads in Round 1 ✅

What changed

  1. Biome includes expanded

    • Updated biome.json to lint all source files via src/**/*.js (instead of only 3 explicit files).
    • Fixed newly surfaced lint/format issues to keep checks green.
  2. DB schema hardening

    • Added role constraint in src/db.js:
      • role TEXT NOT NULL CHECK (role IN ('user', 'assistant', 'system'))
  3. Migration TODO clarified

    • Added/updated TODO in src/index.js documenting loadState() as migration-only and effectively overwritten when DB hydration runs.
  4. Hydration stale-reference fix

    • In src/modules/ai.js#getHistory, changed async hydration merge to mutate the existing array in-place (instead of replacing with a new array), preventing stale references for existing callers.
  5. Shadowed variable cleanup

    • Removed inner shadowed limit redeclaration in hydration callback.
  6. Unused username column removed from history reads

    • Updated both getHistory and getHistoryAsync queries to select only role, content.
  7. Persistence failure severity + context

    • Upgraded write-through failure logging from warning to error in addToHistory.
    • Added context: channelId, role, username, and full error message.
  8. Startup hydration N+1 eliminated

    • Replaced per-channel query loop with one window-function query using ROW_NUMBER() OVER (PARTITION BY channel_id ORDER BY created_at DESC) and grouped results in memory.
  9. Race-condition regression test added

    • Added test that simulates delayed DB hydration + concurrent addToHistory, then asserts:
      • DB messages remain ordered before concurrent message
      • both data sources are present
      • original returned array reference is updated in-place.
  10. Misnamed header test corrected

  • Renamed test title to match assertion behavior:
    • should include correct headers in fetch request

Validation

  • pnpm test ✅ (47/47 passing)
  • pnpm test:coverage
    • All files: 92.73% statements / 87.64% branches / 88.46% funcs / 93.06% lines
  • npx biome check src/ tests/

All listed review threads were resolved after applying the fixes.

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: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
src/utils/errors.js (1)

51-56: ⚠️ Potential issue | 🟡 Minor

Pre-existing: ETIMEDOUT is caught by the NETWORK branch and never reaches TIMEOUT.

Not introduced in this PR, but worth noting: line 51 matches ETIMEDOUTNETWORK, so the same check on line 54 is dead code for that code value. TIMEOUT is only reachable via the message.includes('timeout') fallback. If the intent is for ETIMEDOUT to classify as TIMEOUT, the order of these two blocks should be swapped.

src/modules/events.js (1)

97-117: 🧹 Nitpick | 🔵 Trivial

Missing error handling around generateResponse — user gets no feedback on failure.

If generateResponse or the subsequent send/reply calls throw, the error propagates as an unhandled rejection (caught only by the global handler on line 137-138). The user who @mentioned the bot receives no feedback. Consider wrapping this block in a try/catch that replies with a user-friendly error message (you already have getUserFriendlyMessage in src/utils/errors.js).

♻️ Suggested error handling
+        try {
           await message.channel.sendTyping();

           const response = await generateResponse(
             message.channel.id,
             cleanContent,
             message.author.username,
             config,
             healthMonitor,
           );

           // Split long responses
           if (needsSplitting(response)) {
             const chunks = splitMessage(response);
             for (const chunk of chunks) {
               await message.channel.send(chunk);
             }
           } else {
             await message.reply(response);
           }
+        } catch (err) {
+          logError('AI response failed', { channelId: message.channel.id, error: err.message });
+          await message.reply('Something went wrong generating a response. Please try again!').catch(() => {});
+        }

         return; // Don't accumulate direct mentions into chime-in buffer
src/modules/ai.js (1)

399-412: ⚠️ Potential issue | 🟠 Major

fetch call to OpenClaw has no timeout — can hang indefinitely.

If the OpenClaw API becomes unresponsive, this fetch will block forever, tying up the message handler. Use AbortSignal.timeout() to cap the wait.

🔧 Suggested fix
     const response = await fetch(OPENCLAW_URL, {
       method: 'POST',
       headers: {
         'Content-Type': 'application/json',
         ...(OPENCLAW_TOKEN && { Authorization: `Bearer ${OPENCLAW_TOKEN}` }),
       },
       body: JSON.stringify({
         model: config.ai?.model || 'claude-sonnet-4-20250514',
         max_tokens: config.ai?.maxTokens || 1024,
         messages: messages,
       }),
+      signal: AbortSignal.timeout(config.ai?.requestTimeoutMs || 30000),
     });
src/index.js (1)

93-110: 🧹 Nitpick | 🔵 Trivial

saveState uses synchronous I/O — acceptable for shutdown, but consider the interaction with DB-backed persistence.

Once loadState() is removed (per the migration TODO), saveState() will still serialize the in-memory map to disk on shutdown. After migration, this file becomes a redundant snapshot of data already in PostgreSQL. Track removing both together.

🤖 Fix all issues with AI agents
In `@src/commands/config.js`:
- Around line 286-294: The code assumes setConfigValue returns the section
object (code around path.split('.').slice(1).reduce(...) assigning leafValue
from updatedSection), which will break silently if setConfigValue starts
returning the full root; add a short comment documenting the expected return
shape of setConfigValue and add a runtime guard: after computing leafValue,
check if leafValue === undefined and if so attempt the traversal from the root
(i.e., without .slice(1)) or log a clear warning/error indicating the unexpected
return shape and fall back to using value; reference setConfigValue,
updatedSection, leafValue and path when applying the fix so the traversal and
the warning are updated in the same block.

In `@src/db.js`:
- Around line 99-113: Add a standalone index on created_at to accelerate TTL
cleanup; specifically, alongside the existing CREATE INDEX for
idx_conversations_channel_created, create (if not exists) an index named
idx_conversations_created_at on the conversations table for the created_at
column so the DELETE query used in ai.js (the TTL cleanup: DELETE FROM
conversations WHERE created_at < NOW() - INTERVAL '1 day' * $1) can use an index
instead of a sequential scan.

In `@src/modules/ai.js`:
- Around line 168-206: getHistoryAsync can run duplicate DB queries when
conversationHistory lacks channelId; introduce a lightweight in-flight map
(e.g., pendingHydrations) keyed by channelId to store the promise for the
ongoing hydration so concurrent callers await the same promise. On entry, if
conversationHistory.has(channelId) return it; else if
pendingHydrations.has(channelId) await and return that promise's result;
otherwise create and store a hydration promise that performs the DB query (using
getPool(), getHistoryLength(), querying conversations), on success set
conversationHistory.set(channelId, history) and info(...), on failure log via
logWarn(...) and ensure the promise resolves to an empty array; finally remove
the entry from pendingHydrations before resolving and return the cached history.

In `@src/modules/welcome.js`:
- Around line 98-100: The catch block currently logs only err.message which
loses the stack; update the catch handler where logError('Welcome error', ...)
is called in src/modules/welcome.js to include the full stack (e.g., add stack:
err.stack or pass the entire err object) in the metadata so the error stack
trace is preserved when calling logError.

In `@tests/modules/ai.test.js`:
- Around line 165-173: The test should also assert that the write-failure is
logged: update the test around addToHistory to mock the logger used by the
module (e.g., console.error or the module's logger) before calling
setPool/addToHistory, trigger the rejected query as already done, and assert the
logger was called with the expected message that includes the write failure
context (channel id 'ch1' and role 'user' or whatever the implementation logs);
adjust the expected log text to match the exact string/format produced by the
addToHistory implementation.
- Around line 83-119: Replace the fragile single microtick flush (the `await
Promise.resolve()` after awaiting hydrationPromise) with a robust wait that
retries until the expected state is observed; use vi.waitFor to await the
condition on historyRef/getHistory('race-channel') instead of relying on one
microtick. Specifically, after resolving hydrationPromise, call vi.waitFor with
an assertion that historyRef equals the expected array (or that
getHistory('race-channel') returns historyRef) so the test waits for any
additional async work triggered by mockQuery/hydrationPromise to complete.
📜 Review details

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 804d662 and 010c8e3.

📒 Files selected for processing (17)
  • biome.json
  • src/commands/config.js
  • src/commands/ping.js
  • src/commands/status.js
  • src/db.js
  • src/index.js
  • src/logger.js
  • src/modules/ai.js
  • src/modules/chimeIn.js
  • src/modules/config.js
  • src/modules/events.js
  • src/modules/spam.js
  • src/modules/welcome.js
  • src/utils/errors.js
  • src/utils/registerCommands.js
  • src/utils/retry.js
  • tests/modules/ai.test.js
🧰 Additional context used
🧠 Learnings (2)
📚 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 **/*.{ts,tsx,js,jsx,json,css,md} : After changing or editing any files, run the complete validation workflow: `pnpm format && pnpm typecheck && pnpm lint && pnpm build` before committing

Applied to files:

  • biome.json
📚 Learning: 2026-02-04T02:20:09.131Z
Learnt from: CR
Repo: VolvoxCommunity/Volvox.Website PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-02-04T02:20:09.131Z
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/modules/welcome.js
  • src/logger.js
🧬 Code graph analysis (9)
src/utils/registerCommands.js (1)
src/logger.js (1)
  • info (215-217)
src/modules/welcome.js (1)
src/logger.js (1)
  • info (215-217)
src/index.js (3)
src/db.js (3)
  • client (82-82)
  • closeDb (144-155)
  • initDb (54-127)
src/modules/ai.js (4)
  • stopConversationCleanup (330-336)
  • setPool (83-85)
  • initConversationHistory (255-305)
  • startConversationCleanup (311-325)
src/logger.js (2)
  • info (215-217)
  • error (229-231)
src/db.js (1)
src/modules/ai.js (8)
  • pool (121-121)
  • pool (174-174)
  • pool (178-184)
  • pool (231-231)
  • pool (256-256)
  • pool (266-276)
  • pool (313-313)
  • pool (343-343)
src/modules/events.js (1)
src/logger.js (2)
  • info (215-217)
  • warn (222-224)
src/modules/ai.js (3)
src/logger.js (2)
  • config (27-27)
  • info (215-217)
src/modules/config.js (5)
  • getConfig (130-132)
  • pool (58-58)
  • pool (74-74)
  • pool (166-166)
  • pool (243-243)
src/db.js (2)
  • getPool (134-139)
  • pool (12-12)
src/commands/config.js (1)
src/modules/config.js (2)
  • section (150-150)
  • err (30-30)
tests/modules/ai.test.js (4)
src/modules/ai.js (6)
  • OPENCLAW_URL (104-107)
  • OPENCLAW_URL (104-107)
  • OPENCLAW_TOKEN (108-108)
  • OPENCLAW_TOKEN (108-108)
  • config (31-31)
  • config (46-46)
src/modules/config.js (2)
  • getConfig (130-132)
  • i (360-360)
src/logger.js (2)
  • warn (222-224)
  • info (215-217)
src/index.js (1)
  • config (50-50)
src/modules/config.js (4)
src/commands/config.js (4)
  • value (268-268)
  • section (189-189)
  • section (271-271)
  • section (321-321)
src/index.js (1)
  • client (53-61)
src/db.js (1)
  • client (82-82)
src/logger.js (1)
  • info (215-217)
⏰ 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 (28)
src/utils/errors.js (1)

118-156: Formatting-only changes look good.

The reformatted message map entries are cleaner and consistent with the project's new Biome formatting rules.

src/modules/spam.js (1)

27-51: Formatting-only changes — LGTM.

Arrow function parentheses, multi-line .fetch(), lowercase hex color, and trailing comma are all consistent with the new Biome formatting rules. No behavioral impact.

src/modules/chimeIn.js (2)

12-14: Import reordering — LGTM.


42-61: Formatting-only changes throughout — LGTM.

Object literal restructuring, single-line .join() calls, and sort callback reformatting are all cosmetic. No behavioral impact.

Also applies to: 92-92, 149-149

src/commands/ping.js (1)

7-17: Formatting-only changes — LGTM.

Trailing comma and single-line template string are consistent with Biome config. No behavioral impact.

src/modules/events.js (2)

6-11: Logging and utility imports look good.

Clean migration from console.* to structured logger functions, and proper import of message-splitting utilities.


132-140: Error handlers properly migrated to structured logging — LGTM.

Defensive err?.message || String(err) handles non-Error rejections. Including stack traces aids debugging.

src/commands/config.js (3)

76-114: Well-structured recursive path collector — LGTM.

collectConfigPaths correctly handles leaf values, nested objects, arrays (including empty ones), and empty objects. The accumulator pattern avoids unnecessary allocations.


218-251: Good addition: embed size tracking prevents Discord API errors.

Tracking cumulative field length against EMBED_CHAR_LIMIT - 200 and adding a truncation notice is a solid defensive pattern for avoiding Discord's 6000-character embed limit.


14-16: Nice defensive addition for inline code injection.

escapeInlineCode prevents user-supplied section names from breaking Discord's backtick formatting — good practice for any user-controlled string embedded in markdown.

biome.json (1)

1-34: ⚠️ Potential issue | 🔴 Critical

Update schema version to 2.3.11 — the version referenced (2.3.14) does not exist. The latest Biome CLI is 2.3.11 as of February 2026. Update the $schema URL to "https://biomejs.dev/schemas/2.3.11/schema.json".

The rest of the configuration is well-structured: the src/**/*.js and tests/**/*.js globs cover all source and test files, the noConsole: "error" rule is appropriate given the centralized logger usage, and the formatter settings are consistent with the changes in this PR.

⛔ Skipped due to learnings
Learnt from: CR
Repo: VolvoxCommunity/Volvox.Website PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-26T01:57:34.920Z
Learning: Applies to **/*.{ts,tsx,js,jsx,json,css,md} : After changing or editing any files, run the complete validation workflow: `pnpm format && pnpm typecheck && pnpm lint && pnpm build` before committing
src/logger.js (2)

11-13: Good modernization of imports to node: protocol.

Using node:fs, node:path, and node:url prefixes is the recommended practice for Node.js built-in modules in ESM, making it explicit these are core modules.


102-102: Good switch to Object.hasOwn.

Object.hasOwn is safer and more readable than hasOwnProperty checks.

src/utils/retry.js (1)

1-130: Formatting and style-only changes look good.

Import reorder, exponentiation operator, and arrow function formatting are all clean improvements with no behavioral impact.

src/utils/registerCommands.js (1)

1-59: Clean migration to structured logging with proper error propagation.

The error is logged and re-thrown, preserving the stack for callers. Formatting and REST call restructuring look correct.

src/modules/config.js (3)

26-40: loadConfigFromFile caches on first successful parse — subsequent changes to config.json require a restart.

This is documented by the comment on line 18 ("loaded once, never invalidated"), which is appropriate for a seed/fallback file. The implementation is clean.


96-105: Transaction handling is correct with proper cleanup.

ROLLBACK in catch, client.release() in finally — this pattern is repeated consistently throughout the file. The /* ignore rollback failure */ comments are acceptable since the original error is re-thrown.


332-346: Good prototype pollution protection.

Checking __proto__, constructor, and prototype in both validatePathSegments and setNestedValue provides defense-in-depth against path-based prototype pollution attacks on config mutation.

src/commands/status.js (1)

139-152: Robust error handling with deferred/replied state awareness.

The pattern of checking interaction.replied || interaction.deferred before choosing followUp vs reply correctly handles all interaction states. The .catch(() => {}) swallows any secondary failure, which is appropriate for a last-resort error handler.

src/modules/ai.js (2)

116-161: Previous stale-reference and shadowed-variable issues have been addressed.

The in-place mutation pattern (arr.length = 0; arr.push(...)) on lines 143-144 correctly preserves references for callers, and the redundant limit declaration has been removed. The unused username column has also been dropped from the query.


239-246: Write-through failure now logs at logError level — previous concern addressed.

The upgrade from logWarn to logError (line 240) properly surfaces persistence failures for alerting.

src/index.js (2)

232-274: Shutdown sequencing is well-ordered.

The sequence — drain pending requests → stop cleanup timer → save state → close DB → destroy client — is correct and avoids race conditions between the cleanup scheduler and DB pool teardown.


313-349: Startup sequence is clean with proper dependency ordering.

DB init → config load → pool injection → state hydration → event registration → login. The loadState() migration TODO is acknowledged. One observation: startConversationCleanup() (line 341) is called unconditionally but guards internally against missing DB — this is fine but could be guarded here for clarity.

tests/modules/ai.test.js (5)

1-53: Clean test scaffolding with proper isolation.

State reset in beforeEach (new Map, null pool, clear mocks) and afterEach (stop cleanup, restore mocks) ensures good test isolation. The mock defaults align with the documented config defaults.


198-255: Solid async history retrieval coverage.

Cache-first, DB fallback with reversal, empty results, error logging, and no-pool scenarios are all well covered. The assertion on the LIMIT parameter (line 224) correctly validates the config integration.


257-319: Good hydration test coverage validating the N+1 fix.

Line 288–289 correctly asserts a single query using ROW_NUMBER(), confirming the optimized hydration path. Error and empty-result edge cases are covered.


321-379: Cleanup scheduler tests are well-structured.

Good use of vi.waitFor for async assertions and proper stopConversationCleanup() teardown in each test.


555-580: Good fallback coverage ensuring resilience when config is unavailable.

The getConfig throwing test (line 568) validates the default historyLength of 20 is applied, which is critical for the graceful degradation path.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

@cursor

This comment has been minimized.

coderabbitai[bot]
coderabbitai bot previously approved these changes Feb 11, 2026
@BillChirico BillChirico merged commit c5b484b into main Feb 11, 2026
2 of 3 checks passed
@BillChirico BillChirico deleted the feat/conversation-persistence branch February 11, 2026 19:15
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: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
src/modules/events.js (1)

149-155: 🧹 Nitpick | 🔵 Trivial

JSDoc @param type for client not updated in registerEventHandlers.

The other handler functions were updated to @param {Client} client, but this one still says @param {Object} client.

🧹 Proposed fix
 /**
  * Register all event handlers
- * `@param` {Object} client - Discord client
+ * `@param` {Client} client - Discord client
  * `@param` {Object} config - Bot configuration
  * `@param` {Object} healthMonitor - Health monitor instance
  */
src/index.js (1)

273-287: 🧹 Nitpick | 🔵 Trivial

Duplicate error and unhandledRejection handlers with events.js.

client.on('error', ...) (Line 273) duplicates client.on(Events.Error, ...) in src/modules/events.js Line 137 — both fire on every client error. The unhandledRejection handler (Line 281) is mitigated by the processHandlersRegistered guard in events.js, but the client error handler has no such guard.

Consider consolidating error handling in one location to avoid double-logging.

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

674-895: ⚠️ Potential issue | 🔴 Critical

Remove this entire duplicate test suite — it breaks CI.

Lines 674–895 are a stale copy of the old test suite appended after the new one. This causes the pipeline failures:

  • noRedeclare: afterEach, beforeEach, describe, etc. are re-imported
  • organizeImports: the second import block is unsorted
  • It also re-invokes vi.mock('../../src/logger.js') and imports the removed getHistory API

Delete everything from line 674 onward.

🔧 Proposed fix
-import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
-
-// Mock logger
-vi.mock('../../src/logger.js', () => ({
-  info: vi.fn(),
-  error: vi.fn(),
-  warn: vi.fn(),
-  debug: vi.fn(),
-}));
-
-import {
-  addToHistory,
-  generateResponse,
-  getConversationHistory,
-  getHistory,
-  OPENCLAW_TOKEN,
-  OPENCLAW_URL,
-  setConversationHistory,
-} from '../../src/modules/ai.js';
-
-describe('ai module', () => {
-  ... (all lines 674-895)
-});

Remove lines 674 through 895 entirely.

🤖 Fix all issues with AI agents
In @.gitignore:
- Around line 21-22: Remove the redundant duplicate entry "coverage/" from
.gitignore (the duplicate found between lines shown in the diff); keep the
original listing and delete the second "coverage/" line so the file only
contains one entry for coverage.

In `@src/modules/ai.js`:
- Around line 347-368: The runCleanup function currently catches DB errors using
logWarn which is inconsistent with addToHistory's error-level logging; update
the catch block in runCleanup to use the same error-level logger used by
addToHistory (replace logWarn('Conversation cleanup failed', { error:
err.message }) with the error-level logging call used elsewhere, include the
error details and same context keys), keeping getPool, getHistoryTTLDays and the
surrounding try/catch intact.

In `@tests/db.test.js`:
- Line 177: The file contains duplicated import declarations for the Vitest
helpers (afterEach, beforeEach, describe, expect, it, vi) — remove the redundant
import block and the superseded first test suite so the helpers are only
imported once (or alternatively split the two suites into separate test files);
locate the duplicate import of afterEach/beforeEach/describe/expect/it/vi and
the surrounding first suite and delete that whole initial suite (or move it into
its own file) so lint noRedeclare and import-sorting errors are resolved.

In `@tests/modules/ai.test.js`:
- Around line 69-125: Tests define duplicate describe('getHistoryAsync') blocks
which should be merged to avoid confusing duplicate suite names; locate the two
describe('getHistoryAsync') suites and consolidate their it(...) cases into a
single describe('getHistoryAsync') block (keeping tests: "should create empty
history for new channel", "should return existing history for known channel",
and the concurrency/hydration test that uses getHistoryAsync,
getConversationHistory, setPool, addToHistory and vi mocks) then remove the
redundant describe block so all related it tests run under one suite and
maintain shared setup/teardown if any.
📜 Review details

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 36baa91 and eb513b3.

📒 Files selected for processing (10)
  • .gitignore
  • config.json
  • package.json
  • src/db.js
  • src/index.js
  • src/modules/ai.js
  • src/modules/events.js
  • tests/db.test.js
  • tests/index.test.js
  • tests/modules/ai.test.js
🧰 Additional context used
📓 Path-based instructions (3)
src/**/*.js

📄 CodeRabbit inference engine (AGENTS.md)

src/**/*.js: Use ESM syntax with import/export; never use require()
Always use node: protocol when importing Node.js builtins (e.g., import { readFileSync } from 'node:fs')
Always use semicolons in code
Use single quotes for strings (enforced by Biome)
Use 2-space indentation (enforced by Biome)
Always use Winston logging via import { info, warn, error } from '../logger.js' — NEVER use console.log(), console.warn(), console.error(), or any console.* method in src/ files
Pass structured metadata when logging: info('Message processed', { userId, channelId })
Use custom error classes from src/utils/errors.js for error handling
Always log errors with context before re-throwing
Use getConfig() from src/modules/config.js to read config; use setConfigValue(key, value) to update at runtime
Use splitMessage() utility for messages exceeding Discord's 2000-character limit
No TypeScript — use plain JavaScript with JSDoc comments for documentation

Files:

  • src/modules/events.js
  • src/db.js
  • src/index.js
  • src/modules/ai.js
src/modules/*.js

📄 CodeRabbit inference engine (AGENTS.md)

Check config.yourModule.enabled before processing in module handlers

Files:

  • src/modules/events.js
  • src/modules/ai.js
tests/**/*.{js,test.js}

📄 CodeRabbit inference engine (AGENTS.md)

Use Vitest for testing with pnpm test; test coverage provider is @vitest/coverage-v8

Files:

  • tests/db.test.js
  • tests/index.test.js
  • tests/modules/ai.test.js
🧠 Learnings (10)
📚 Learning: 2026-02-11T17:18:29.603Z
Learnt from: CR
Repo: BillChirico/bills-bot PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-02-11T17:18:29.603Z
Learning: Module handlers must be registered in `src/modules/events.js` to wire them to Discord events

Applied to files:

  • src/modules/events.js
📚 Learning: 2026-02-11T17:18:29.603Z
Learnt from: CR
Repo: BillChirico/bills-bot PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-02-11T17:18:29.603Z
Learning: Graceful shutdown is handled in `src/index.js`; use error classes and logging before throwing errors in other modules

Applied to files:

  • src/modules/events.js
  • src/index.js
📚 Learning: 2026-02-11T17:18:29.603Z
Learnt from: CR
Repo: BillChirico/bills-bot PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-02-11T17:18:29.603Z
Learning: Applies to src/**/*.js : Use `splitMessage()` utility for messages exceeding Discord's 2000-character limit

Applied to files:

  • src/modules/events.js
📚 Learning: 2026-02-04T02:20:09.131Z
Learnt from: CR
Repo: VolvoxCommunity/Volvox.Website PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-02-04T02:20:09.131Z
Learning: Applies to {package.json,.npmrc,pnpm-lock.yaml} : Use pnpm package manager (pinned to v10.23.0) with `.npmrc` configured for strict peer dependencies and disabled shamefully-hoist

Applied to files:

  • package.json
📚 Learning: 2026-02-11T17:18:29.603Z
Learnt from: CR
Repo: BillChirico/bills-bot PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-02-11T17:18:29.603Z
Learning: Applies to tests/**/*.{js,test.js} : Use Vitest for testing with `pnpm test`; test coverage provider is `vitest/coverage-v8`

Applied to files:

  • package.json
  • tests/db.test.js
  • .gitignore
📚 Learning: 2025-10-10T15:05:26.145Z
Learnt from: CR
Repo: BillChirico/LUA-Obfuscator PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-10-10T15:05:26.145Z
Learning: Applies to package.json : Only add new packages when absolutely necessary or explicitly requested

Applied to files:

  • package.json
📚 Learning: 2026-02-11T17:18:29.603Z
Learnt from: CR
Repo: BillChirico/bills-bot PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-02-11T17:18:29.603Z
Learning: Commands are auto-discovered from `src/commands/` on startup; after adding a command, run `pnpm run deploy` to register with Discord

Applied to files:

  • package.json
📚 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 **/*.{ts,tsx,js,jsx,json,css,md} : After changing or editing any files, run the complete validation workflow: `pnpm format && pnpm typecheck && pnpm lint && pnpm build` before committing

Applied to files:

  • package.json
📚 Learning: 2026-02-11T17:18:29.603Z
Learnt from: CR
Repo: BillChirico/bills-bot PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-02-11T17:18:29.603Z
Learning: Applies to src/**/*.js : Use single quotes for strings (enforced by Biome)

Applied to files:

  • package.json
📚 Learning: 2026-02-11T17:18:29.603Z
Learnt from: CR
Repo: BillChirico/bills-bot PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-02-11T17:18:29.603Z
Learning: Applies to src/**/*.js : Use 2-space indentation (enforced by Biome)

Applied to files:

  • package.json
🧬 Code graph analysis (5)
src/modules/events.js (1)
src/index.js (1)
  • client (54-62)
tests/db.test.js (1)
src/db.js (4)
  • initDb (54-132)
  • getPool (139-144)
  • closeDb (149-160)
  • pool (12-12)
tests/modules/ai.test.js (1)
src/modules/config.js (2)
  • getConfig (130-132)
  • i (360-360)
src/db.js (1)
src/modules/config.js (4)
  • pool (58-58)
  • pool (74-74)
  • pool (166-166)
  • pool (243-243)
src/index.js (1)
src/modules/ai.js (4)
  • stopConversationCleanup (335-341)
  • setPool (86-88)
  • initConversationHistory (249-309)
  • startConversationCleanup (315-330)
🪛 GitHub Actions: CI
tests/db.test.js

[error] 1-1: assist/source/organizeImports: The imports and exports are not sorted. Safe fix: Organize Imports (Biome).


[error] 177-177: lint/suspicious/noRedeclare: Shouldn't redeclare 'afterEach'.


[error] 177-177: lint/suspicious/noRedeclare: Shouldn't redeclare 'beforeEach'.


[error] 177-177: lint/suspicious/noRedeclare: Shouldn't redeclare 'describe'.


[error] 177-177: lint/suspicious/noRedeclare: Shouldn't redeclare 'expect'.


[error] 177-177: lint/suspicious/noRedeclare: Shouldn't redeclare 'it'.


[error] 177-177: lint/suspicious/noRedeclare: Shouldn't redeclare 'vi'.

tests/modules/ai.test.js

[error] 674-674: assist/source/organizeImports: The imports and exports are not sorted. Safe fix: Organize Imports (Biome).


[error] 674-674: lint/suspicious/noRedeclare: Shouldn't redeclare 'afterEach'.

⏰ 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 (16)
config.json (1)

7-9: LGTM!

New config fields historyLength and historyTTLDays with sensible defaults that match the fallback constants in src/modules/ai.js.

src/db.js (1)

54-131: LGTM!

The reordered guard logic, conversations table schema with CHECK constraint, composite index, and standalone created_at index for TTL cleanup are all sound. The error path properly cleans up the pool on schema creation failure.

package.json (1)

1-38: LGTM!

Script additions (test:watch) and dependency updates look reasonable.

src/modules/events.js (1)

6-6: LGTM — Using Events constants instead of string literals.

Good improvement for type safety and refactoring resilience.

src/modules/ai.js (6)

120-183: Hydration logic is well-structured with proper deduplication and in-place mutation.

The pendingHydrations map prevents duplicate DB queries for the same channel, the in-place array mutation preserves references for concurrent callers, and the merge ordering (DB first, then in-flight writes) is correct. Previous review concerns about shadowed variables, unused username column, N+1 queries, and concurrent hydration deduplication have all been addressed.


224-241: Fire-and-forget DB write now logs at error level with context — good.

Previous review concern about warn vs error level for persistence failures has been addressed.


249-309: LGTM — Single-query hydration with ROW_NUMBER() window function.

Addresses the previous N+1 query concern. The TTL filter in the subquery avoids scanning expired rows, and in-place replacement prevents duplicate growth across restarts.


315-330: LGTM — cleanupTimer.unref() applied as previously suggested.


438-440: User message content is prefixed with username in history.

addToHistory(channelId, 'user', ${username}: ${userMessage}, username) stores "username: message" as the content. When this is hydrated from DB and then sent back to the API (Line 399), the model will see "username: message" in the history — which is the intended format matching the current message on Line 399. This is consistent.


86-88: No issues found. The setPool function at lines 86-88 is correctly implemented with the parameter name matching the assignment (pool_poolRef = pool).

src/index.js (2)

304-332: Startup sequence is well-ordered and handles the no-DB fallback gracefully.

The flow correctly: initializes DB → loads config → sets pool on AI module → loads file state (migration path) → hydrates from DB (overwrites file state) → starts cleanup → registers events → loads commands → logs in. The TODO on Lines 323-325 clearly documents the migration intent.


244-266: Shutdown sequence correctly stops cleanup before saving state and closing DB.

The ordering ensures no cleanup queries run against a closed pool, and state is persisted before the pool is torn down.

tests/db.test.js (1)

222-369: Second test suite (Lines 222–369) is comprehensive and well-structured.

Good coverage of init, idempotency, concurrent rejection, getPool, closeDb (including error handling), schema validation, and SSL configuration variants. The class-based Pool mock properly captures config for SSL assertions. The re-entrancy guard test and connection failure cleanup test are good additions addressing prior review feedback.

tests/index.test.js (1)

31-34: LGTM — mock additions are consistent and complete.

New AI lifecycle mocks (setPool, initConversationHistory, startConversationCleanup, stopConversationCleanup) are properly declared, wired into vi.mock, and reset in importIndex. Clean and symmetric with existing patterns.

Also applies to: 126-129, 194-197

tests/modules/ai.test.js (2)

82-124: Good coverage of the hydration race condition.

This test properly exercises the concurrent-write-during-hydration scenario and uses vi.waitFor for resilience, addressing prior feedback.


170-190: Good — DB write failure now asserts error logging with context.

Addresses the prior feedback about verifying that write-through failures are logged with structured context (channelId, role, username, error).

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Comment on lines +21 to +22
# Test and coverage outputs
coverage/
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Duplicate coverage/ entry.

coverage/ is already listed on Line 5. This second entry is redundant.

🧹 Proposed fix
-# Test and coverage outputs
-coverage/
-
📝 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.

Suggested change
# Test and coverage outputs
coverage/
🤖 Prompt for AI Agents
In @.gitignore around lines 21 - 22, Remove the redundant duplicate entry
"coverage/" from .gitignore (the duplicate found between lines shown in the
diff); keep the original listing and delete the second "coverage/" line so the
file only contains one entry for coverage.

Comment on lines +347 to 368
async function runCleanup() {
const pool = getPool();
if (!pool) return;

try {
const ttlDays = getHistoryTTLDays();
const result = await pool.query(
`DELETE FROM conversations
WHERE created_at < NOW() - INTERVAL '1 day' * $1`,
[ttlDays],
);

if (result.rowCount > 0) {
info('Cleaned up old conversation messages', {
deleted: result.rowCount,
ttlDays,
});
}
} catch (err) {
logWarn('Conversation cleanup failed', { error: err.message });
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Cleanup failure logged at warn — consider error for consistency.

addToHistory DB failures are logged at error level (Line 234), but runCleanup DB failures use logWarn (Line 366). Both represent persistence-layer failures; using the same severity would be more consistent for alerting.

🤖 Prompt for AI Agents
In `@src/modules/ai.js` around lines 347 - 368, The runCleanup function currently
catches DB errors using logWarn which is inconsistent with addToHistory's
error-level logging; update the catch block in runCleanup to use the same
error-level logger used by addToHistory (replace logWarn('Conversation cleanup
failed', { error: err.message }) with the error-level logging call used
elsewhere, include the error details and same context keys), keeping getPool,
getHistoryTTLDays and the surrounding try/catch intact.

AnExiledDev referenced this pull request in AnExiledDev/bills-bot Feb 12, 2026
Resolve six issues identified during review of the last five merged PRs:

- Wrap Discord message sends in try-catch with user-friendly fallback
- Invert NODE_ENV check to hide error details by default
- Remove duplicate unhandledRejection handler from index.js
- Remove dead graceful-shutdown request tracking code
- Null out ChimeIn AbortController in finally block
- Add throttled eviction to welcome activity Map
BillChirico pushed a commit that referenced this pull request Feb 12, 2026
Resolve six issues identified during review of the last five merged PRs:

- Wrap Discord message sends in try-catch with user-friendly fallback
- Invert NODE_ENV check to hide error details by default
- Remove duplicate unhandledRejection handler from index.js
- Remove dead graceful-shutdown request tracking code
- Null out ChimeIn AbortController in finally block
- Add throttled eviction to welcome activity Map
BillChirico added a commit that referenced this pull request Feb 16, 2026
- Test refreshDiscordToken: success, failure, token rotation, no refresh token (issue #15)
- Test fetchWithRateLimit: 429 response, retry-after parsing, max retries exhaustion (issue #16)
- Test proxy: actual redirect behavior for unauthenticated requests, passthrough
  for valid tokens, callbackUrl preservation (issue #17)
- Fix mock types: complete Account/Token objects with all required fields (issue #18)
- Update guilds API test to use getToken() instead of session.accessToken
- Update landing page tests for conditional invite button rendering
- Update header/dashboard-shell/server-selector tests for refactored components
- Add health check endpoint test
- All 82 tests passing, lint clean, build succeeds
BillChirico added a commit that referenced this pull request Feb 17, 2026
- Export _seedOAuthState() from auth.js for test-seeding CSRF states
- Add integration test verifying full OAuth callback flow:
  code exchange → user fetch → session store → JWT redirect
- Verifies session stored server-side and JWT contains correct user info

Addresses PR #73 review thread #18
BillChirico added a commit that referenced this pull request Feb 17, 2026
- Export _seedOAuthState() from auth.js for test-seeding CSRF states
- Add integration test verifying full OAuth callback flow:
  code exchange → user fetch → session store → JWT redirect
- Verifies session stored server-side and JWT contains correct user info

Addresses PR #73 review thread #18
BillChirico added a commit that referenced this pull request Feb 25, 2026
… (#83)

* feat(dashboard): add Discord entity pickers

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat(dashboard): add diff view and polish

- ConfigDiff.tsx: visual diff component showing before/after config changes
  with green additions and red deletions using the `diff` library
- SystemPromptEditor.tsx: textarea with real-time character count, max
  length warning indicator, and accessible labeling
- Toast notifications via sonner: success/error toasts on save, load
  failures, and reset actions positioned bottom-right
- ResetDefaultsButton with confirmation dialog using Radix UI Dialog
- ConfigEditor.tsx: full config editing page with AI, welcome message,
  and moderation sections; PATCH-based save with diff preview
- Config API proxy route (GET/PATCH) following established analytics
  proxy pattern with guild admin authorization
- Dialog UI component (shadcn/ui new-york style)
- Added lint script to web/package.json

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* polish(config-editor): improve UX, accessibility, and edge case handling

- Replace native checkboxes with styled toggle switches using proper
  role="switch" and aria-checked attributes
- Add unsaved changes guard (beforeunload warning + yellow banner)
- Add Ctrl/Cmd+S keyboard shortcut for saving
- Block save when system prompt exceeds character limit
- Rename misleading "Reset to Defaults" to "Discard Changes" with
  accurate dialog copy (reverts to last saved state, not factory defaults)
- Add diff summary counts (+N / -N) to the pending changes card
- Improve accessibility throughout: aria-labels on loading spinner,
  aria-describedby linking textareas to their hints, aria-invalid on
  over-limit prompt, aria-live on character counter, aria-hidden on
  decorative icons, role="region" on diff view
- Memoize hasChanges and hasValidationErrors to avoid redundant
  JSON.stringify on every render
- Validate PATCH body shape in API route before proxying upstream
- Fix stale "bills-bot" prefix in guild-selection localStorage keys
  (missed during volvox rename)

Closes #31

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat(api): add config endpoints for Issue #31

Add REST API endpoints for managing bot configuration:
- GET /api/v1/config - Retrieve current config (ai, welcome, spam, moderation)
- PUT /api/v1/config - Update config with schema validation

Features:
- Type-safe schema validation for config sections
- Flatten nested objects to dot-notation paths for persistence
- requireGlobalAdmin middleware (API-secret or bot-owner OAuth)
- Proper HTTP error codes (400 for validation, 401/403 for auth, 500 for errors)
- Added PUT to CORS methods

Tests:
- 35 comprehensive tests covering auth, validation, types, edge cases
- Tests for validateConfigSchema and flattenToLeafPaths exports

Closes #31

* feat(api): add webhook notifications for config changes

- Add notifyDashboardWebhook() fire-and-forget sender to PATCH /:id/config
- POST /webhooks/config-update endpoint for dashboard to push config changes
- Webhook uses DASHBOARD_WEBHOOK_URL env var with 5s timeout
- Add comprehensive tests for webhook functionality

* feat(dashboard): add config editor with Zustand state management

Add a full config editor UI at /dashboard/config with:
- Proxy API routes (GET + PATCH) for bot config at /api/guilds/:guildId/config
- Zustand store for config state with fetch, update, and debounced saves
- Accordion-based sections for ai, welcome, spam, moderation (read-only), triage
- Recursive field renderer supporting booleans, numbers, strings, arrays, objects
- shadcn/ui components: accordion, input, label, switch, textarea

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat(dashboard): enhance Discord entity pickers with multi-select and Zustand

- Add multi-select mode to ChannelSelector and RoleSelector (multiple?: boolean prop)
- Create Zustand store for caching channels/roles per guild
- Add dedicated bot API endpoints: GET /:id/channels and GET /:id/roles
- Add Next.js proxy routes for channels and roles
- Update AGENTS.md with new key files

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: remove unused import in webhooks.js

* fix: resolve all linting errors across codebase

- Migrate biome config from 2.3.14 to 2.4.0
- Fix unused imports (triage.js, modAction.test.js)
- Fix import ordering (auth.js, lock.js, unlock.js, ai.js, triage-respond.js, modAction.js, modAction.test.js)
- Fix formatting across 19 files
- Replace O(n²) spread in reduce with push (cli-process.test.js)
- Use Object.hasOwn() instead of Object.prototype.hasOwnProperty (config-guild.test.js)

All 1310 tests pass.

* feat: add full config editing support for moderation and triage

- Add moderation and triage to SAFE_CONFIG_KEYS in guilds.js, webhooks.js,
  and config.js making them writable via PATCH/PUT endpoints
- Expand READABLE_CONFIG_KEYS to include all sections: ai, welcome, spam,
  moderation, triage, logging, memory, permissions
- Add CONFIG_SCHEMA definitions for moderation and triage sections with
  full type validation
- Update WritableConfigSection type to include moderation and triage
- Remove moderation from READ_ONLY_SECTIONS in config-section.tsx
- Update config-store.ts writable keys check
- Add editable moderation section in dashboard config-editor with toggles
  for enabled, autoDelete, DM notifications, and escalation
- Add editable triage section with fields for models, budgets, intervals,
  streaming, debug footer, and moderation log channel
- Update all test assertions to reflect new writable sections

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(security): add webhook URL validation, schema validation on all write paths, atomic writes

- Add shared validateWebhookUrl() utility that blocks SSRF (localhost,
  private IPs, link-local, IPv6 loopback) and enforces https in production
- Wire URL validation into config.js notifyWebhook and guilds.js
  notifyDashboardWebhook
- Export validateSingleValue() from config.js and apply it to the PATCH
  endpoint in guilds.js and the POST /config-update webhook endpoint
- Add path length (<=200 chars) and depth (<=10 segments) limits to
  guilds.js PATCH and webhooks.js POST endpoints
- Refactor PUT handler in config.js to track per-field write results:
  returns 200 on full success, 207 on partial failure, 500 when all fail
- Add comprehensive tests for all new validations and error responses

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor: extract shared config allowlist, webhook utility, and proxy helpers; remove dead code

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(frontend): batch saves, fix race conditions, DRY constants, localStorage safety

- Batch saveChanges into parallel PATCH requests grouped by section
  instead of sequential individual PATCHes (I5)
- Add request sequence counter to Zustand config store to prevent
  stale PATCH responses from clobbering newer state (I6)
- Centralize SYSTEM_PROMPT_MAX_LENGTH constant in types/config.ts
  and import in config-editor and system-prompt-editor (M2)
- Wrap localStorage.getItem in try/catch for SSR safety (M3)
- Fix channels.length / roles.length truthiness bug — use
  !== undefined instead of .length which is falsy for 0 (M5)
- Replace JSON.stringify deep comparison with recursive deepEqual
  utility function (M8)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(security): mask sensitive config fields, block IPv4-mapped IPv6 SSRF, reject unknown config paths

- Add SENSITIVE_FIELDS set and maskSensitiveFields utility to strip triage
  API keys (classifyApiKey, respondApiKey) from all GET config responses
- Block SSRF via IPv4-mapped IPv6 addresses (::ffff:127.0.0.1, hex form
  ::ffff:7f00:1, and cloud metadata ::ffff:169.254.169.254)
- Reject unknown config paths in validateSingleValue instead of silently
  accepting them without type checking
- Add cache size limit (100 entries) to webhook URL validation cache
- Guard flattenToLeafPaths against __proto__/constructor/prototype keys

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor(backend): extract shared validators and getBotOwnerIds, add webhook utility tests

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(frontend): remove dead code, fix save flow, harden inputs and type guards

- C4: delete 10 unused files (stores, UI components, dashboard selectors) and
  remove zustand, @radix-ui/react-accordion, @radix-ui/react-label,
  @radix-ui/react-switch from package.json
- m8: replace ~85-line local GuildConfig interface with DeepPartial<BotConfig>
  (add DeepPartial utility to types/config.ts)
- m4: harden isGuildConfig type guard to verify at least one expected section
  key (ai, welcome, spam) instead of just typeof === "object"
- M6: fix computePatches to include top-level paths (remove incorrect
  fullPath.includes(".") guard that silently dropped top-level field changes)
- M7: fix partial save to merge only succeeded sections into savedConfig on
  partial failure, preserving draft edits for failed sections; only call
  fetchConfig() on full success
- m5: add min constraints to triage number inputs (budgets min=0, timeouts
  min=1, buffer/context sizes min=1)
- m9: add e.returnValue = "" to beforeunload handler for modern browser support

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test: fix 10-segment test to use valid schema path after strict validation

* fix: wire default-personality.md as system prompt fallback

Replace generic 'You are a helpful Discord bot.' fallback with the
existing default-personality.md template, which provides the intended
Volvox Bot personality, role constraints, and anti-abuse guardrails.

* fix: merge default-personality into responder system prompt, remove dead template

Merge personality, role, constraints, and anti-abuse from the unused
default-personality.md into triage-respond-system.md (the responder's
actual system prompt). Revert the fallback wiring in triage-prompt.js
since personality now lives in the system prompt file where it belongs.
Delete default-personality.md — no longer needed.

* 📝 Add docstrings to `feat/config-editor-combined`

Docstrings generation was requested by @BillChirico.

The following files were modified:

* `src/api/routes/auth.js`
* `src/api/routes/config.js`
* `src/api/routes/guilds.js`
* `src/api/utils/configAllowlist.js`
* `src/api/utils/validateConfigPatch.js`
* `src/api/utils/validateWebhookUrl.js`
* `src/commands/lock.js`
* `src/commands/slowmode.js`
* `src/commands/unlock.js`
* `src/modules/memory.js`
* `src/modules/triage-prompt.js`
* `src/modules/triage-respond.js`
* `src/modules/triage.js`
* `src/utils/debugFooter.js`
* `src/utils/permissions.js`
* `web/src/app/api/guilds/[guildId]/channels/route.ts`
* `web/src/app/api/guilds/[guildId]/config/route.ts`
* `web/src/app/api/guilds/[guildId]/roles/route.ts`
* `web/src/app/dashboard/config/page.tsx`
* `web/src/components/dashboard/config-diff.tsx`
* `web/src/components/dashboard/config-editor.tsx`
* `web/src/components/dashboard/reset-defaults-button.tsx`
* `web/src/components/dashboard/system-prompt-editor.tsx`
* `web/src/components/providers.tsx`
* `web/src/lib/bot-api-proxy.ts`

These files were kept as they were:
* `src/api/server.js`
* `src/api/utils/webhook.js`

These files were ignored:
* `tests/api/routes/config.test.js`
* `tests/api/routes/guilds.test.js`
* `tests/api/routes/webhooks.test.js`
* `tests/api/utils/validateWebhookUrl.test.js`
* `tests/api/utils/webhook.test.js`
* `tests/commands/tempban.test.js`
* `tests/config-listeners.test.js`
* `tests/modules/cli-process.test.js`
* `tests/modules/config-guild.test.js`
* `tests/utils/modAction.test.js`

These file types are not supported:
* `.env.example`
* `AGENTS.md`
* `biome.json`
* `src/prompts/triage-respond-system.md`
* `web/package.json`

* fix(prompts): replace ambiguous 'classified' with 'triaged' (Thread #11)

* fix(prompts): define concrete 'flagging' mechanism for moderation (Thread #12)

* fix(prompts): add PII/credentials constraint to prevent echoing secrets (Thread #13)

* fix: remove bracketed-IPv6 dead code in webhook URL validator

URL.hostname already strips brackets from IPv6 addresses
(new URL('http://[::1]').hostname === '::1'), so the
hostname.startsWith('[') branch was unreachable dead code.

Remove the bracketed-IPv6 branch and the '[::1]' entry from
BLOCKED_IPV6 since it could never match.

Addresses CodeRabbit review thread #9.

* fix: sanitize webhook URL in warning logs to prevent credential exposure

Strip query string and fragment from webhook URLs before including
them in warning log messages. If a webhook URL contains tokens or
API keys as query parameters, they would previously appear in logs.

Now logs only origin + pathname (e.g. 'https://example.com/hook')
instead of the full URL with sensitive query params.

Addresses CodeRabbit review thread #10.

* test(config): rename misleading 'Infinity' test name (Thread #14)

* fix: normalize validateSingleValue to always return string[]

Unknown config paths previously returned [{path, message}] objects while
validateValue returned string[]. Callers now receive a consistent string[]
in both cases, eliminating the need to handle two different shapes.

* test(config): move PUT partial write handling inside config routes suite (Thread #15)

* refactor: extract getGuildChannels helper to remove duplication

GET /:id and GET /:id/channels shared identical channel-fetching loops
with a duplicated MAX_CHANNELS constant. Extracted into a single
getGuildChannels(guild) helper used by both handlers.

* feat: include section (topLevelKey) in PATCH config webhook payload

Downstream consumers of the DASHBOARD_WEBHOOK_URL can now use the
'section' field to selectively reload only the affected config section
rather than refreshing the entire config.

* fix: return promise in webhook config-update handler (async/await)

The previous .then()/.catch() chain was not returned, so Express 5
could not auto-forward rejected promises to the error handler. Converted
to async/await so errors propagate correctly.

* feat: make JSON body size limit configurable via API_BODY_LIMIT env var

Defaults to '100kb' when the env var is not set, preserving existing
behaviour. Operators can now tune the limit without code changes.

* test(validateWebhookUrl): strengthen cache eviction test to verify re-evaluation (Thread #16)

* test(webhook): move vi.useRealTimers() to afterEach to prevent timer leak (Thread #17)

* refactor: move CONFIG_SCHEMA/validateValue/validateSingleValue to utils/configValidation.js

validateConfigPatch.js (utils) was importing validateSingleValue from
routes/config.js — an inverted dependency. Created src/api/utils/configValidation.js
as the canonical home for CONFIG_SCHEMA, validateValue, and validateSingleValue.

- config.js now imports from ../utils/configValidation.js and re-exports
  validateSingleValue for backward compatibility with existing callers.
- validateConfigPatch.js now imports from ./configValidation.js directly.

* perf: split path string once in validateConfigPatchBody

path.split('.') was called twice — once to extract topLevelKey and again
for segments. Moved the single split to the top and derived topLevelKey
from segments[0], avoiding the redundant allocation.

* fix(#18): change lint script from tsc to biome check

* fix(#19,#20): simplify params type; add PATCH body value check

* fix(#21): add metadata export to config page

* fix(#22): compute addedCount/removedCount inside useMemo loop

* fix(#23,#24,#25,#26): tighten isGuildConfig; extract inputClasses; guard number inputs; rename DiscardChangesButton

* fix(#27): change aria-live from polite to off on char counter

* fix(#28): change Toaster theme from dark to system

* fix(#29,#30): export BotApiConfig; return 504 on AbortError/TimeoutError

* fix(#31): add one-time localStorage key migration from old key

* fix(#32,#33,#34): SpamConfig JSDoc; collapse WritableConfigSection; fix SYSTEM_PROMPT_MAX_LENGTH JSDoc

* fix: remove unused validateSingleValue import, fix biome formatting in config.js

After the refactor, validateSingleValue is re-exported directly via
'export { } from' and no longer needs a local import binding. Also removed
an extra blank line that biome flagged as a format error.

* fix: add DNS resolution validation to prevent SSRF via DNS rebinding

Add async validateDnsResolution() that resolves a webhook hostname
via DNS and checks all resolved IPs against the existing blocked
ranges before fetch. This closes the TOCTOU gap where a hostname
could pass string-based validation then resolve to a private IP
at request time (DNS rebinding attack).

Changes:
- Add validateDnsResolution() with resolve4/resolve6 checks
- Integrate DNS check in fireAndForgetWebhook before fetch
- Normalize IPv6 hostnames by stripping brackets (Node.js v22
  retains them in URL.hostname, contrary to WHATWG spec)
- Add comprehensive test coverage for DNS rebinding scenarios
- Update webhook tests for async DNS validation flow

Addresses CodeRabbit review thread #8.

* fix: update test assertions for string[] return type from validateSingleValue

* fix: mock validateDnsResolution in webhook integration tests

After adding DNS resolution pinning in fireAndForgetWebhook, the config
and guilds route tests need to mock validateDnsResolution to return true
so fetch is actually called.

* fix: address minor code review feedback - JSDoc, tests, caps

* fix(frontend): address code review feedback - HTML, types, perf

* fix(backend): address code review feedback - validation, logging, exports

* fix: correct IPv6 validation for public addresses and literals

* fix: restore classifier invocation in triage module

* fix: address test failures in validateConfigPatch and triage-respond

- Check for empty path segments before topLevelKey validation
- Fix test to use valid nullable path (welcome.channelId)
- Add mock cleanup between triage-respond tests

* fix(validation): handle alertChannelId nullable and DNS edge cases

* fix(security): prevent mask sentinel write-back and auth secret override

1. configAllowlist: Add isMasked() and stripMaskedWrites() to detect and
   filter out writes where sensitive fields contain the mask sentinel
   ('••••••••'). Prevents clients from accidentally overwriting real
   secrets with the placeholder returned by maskSensitiveFields().

2. bot-api-proxy: Reorder header spread so x-api-secret is always set
   AFTER spreading options.headers, preventing caller-provided headers
   from overriding the server-side auth secret.

Both fixes include comprehensive tests.

* test: add missing test cases for mask sentinel, prototype pollution, DNS edge cases

* refactor: simplify webhook validation for internal-only use

* refactor: remove unused SSRF validation code

Deleted validateWebhookUrl.js and its tests since webhooks are internal-only.
Simplified webhook.js to just check URL format.

* fix: prevent WebSearch notification failures from aborting response

* fix: correct safeSend mock setup in triage-respond tests

* fix(security): use own-property checks in config validation

* fix: export MASK constant and clean up orphaned JSDoc

* fix: report written sections in webhook, add roles endpoint test

* fix: address remaining PR review feedback

- Add nullable: true to triage.moderationLogChannel and debugFooterLevel
- Add evalClient param to runResponder JSDoc
- Convert SAFE_CONFIG_KEYS to Set for O(1) lookups
- Reorder validation checks for consistent 400 responses
- Update tests for Set-based SAFE_CONFIG_KEYS

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: AnExiledDev <AnExiledDev@users.noreply.github.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
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.

1 participant