Skip to content
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
3e40252
feat: add DB-backed AI conversation persistence
BillChirico Feb 11, 2026
08d1da7
fix: prevent race condition in async DB hydration
BillChirico Feb 11, 2026
804d662
chore: remove stray file
BillChirico Feb 11, 2026
8a58e8c
fix: expand biome.json includes to src/**/*.js and fix all lint errors
BillChirico Feb 11, 2026
92b2c19
fix: add CHECK constraint on role column in conversations table
BillChirico Feb 11, 2026
7313073
docs: add migration TODO for loadState in DB startup path
BillChirico Feb 11, 2026
4955beb
fix: harden async history hydration and DB persistence behavior
BillChirico Feb 11, 2026
010c8e3
test: cover hydration race handling and align header test naming
BillChirico Feb 11, 2026
59991e7
fix: guard config set return-shape traversal
BillChirico Feb 11, 2026
afa64bc
fix: add created_at index for conversation ttl cleanup
BillChirico Feb 11, 2026
8fd72d7
fix: dedupe concurrent async history hydrations per channel
BillChirico Feb 11, 2026
6a099f4
fix: include stack traces in welcome error logging
BillChirico Feb 11, 2026
cb40ef3
test: harden hydration race assertion with waitFor
BillChirico Feb 11, 2026
ebb6b44
test: assert DB write failure logging in ai history persistence
BillChirico Feb 11, 2026
c797b66
fix: replace channel history on DB hydration to avoid duplicates
BillChirico Feb 11, 2026
36baa91
chore: sort config command imports for biome
BillChirico Feb 11, 2026
468df96
fix(ai): coordinate sync/async history hydration
BillChirico Feb 11, 2026
ed5283a
fix(ai): unref conversation cleanup timer
BillChirico Feb 11, 2026
929dd5b
test(db): cover initDb re-entrancy guard
BillChirico Feb 11, 2026
3c5fa44
refactor: remove dead code getHistory and update tests to use getHist…
BillChirico Feb 11, 2026
e306ff9
fix: remove duplicate error handlers in index.js
BillChirico Feb 11, 2026
6d9ce1b
fix: add TTL filter to conversation history initialization
BillChirico Feb 11, 2026
eb513b3
chore: resolve merge conflicts with main
BillChirico Feb 11, 2026
252cd9d
fix: align tests with persistence architecture and resolve merge conf…
BillChirico Feb 11, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ logs/security/
data/*
!data/.gitkeep

# Test and coverage outputs
coverage/
Comment on lines +21 to +22
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.


# Verification scripts
verify-*.js
VERIFICATION_GUIDE.md
30 changes: 10 additions & 20 deletions biome.json
Original file line number Diff line number Diff line change
@@ -1,25 +1,19 @@
{
"$schema": "https://biomejs.dev/schemas/2.3.14/schema.json",
"assist": {
"actions": {
"source": {
"organizeImports": "on"
}
}
"files": {
"includes": [
"src/**/*.js",
"tests/**/*.js",
"!node_modules/**",
"!coverage/**",
"!logs/**",
"!data/**"
]
},
"linter": {
"enabled": true,
"rules": {
"recommended": true,
"correctness": {
"noUnusedVariables": "warn",
"noUnusedImports": "warn"
},
"style": {
"useConst": "error"
},
"suspicious": {
"noVar": "error",
"noConsole": "error"
}
}
Expand All @@ -28,17 +22,13 @@
"enabled": true,
"indentStyle": "space",
"indentWidth": 2,
"lineWidth": 100,
"lineEnding": "lf"
"lineWidth": 100
},
"javascript": {
"formatter": {
"quoteStyle": "single",
"trailingCommas": "all",
"semicolons": "always"
}
},
"files": {
"includes": ["**/*.js", "**/*.json", "**/*.md", "!coverage"]
}
}
4 changes: 3 additions & 1 deletion config.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
"model": "claude-sonnet-4-20250514",
"maxTokens": 1024,
"systemPrompt": "You are Volvox Bot, the friendly AI assistant for the Volvox developer community Discord server.\n\nYou're witty, snarky (but warm), and deeply knowledgeable about programming, software development, and tech.\n\nKey traits:\n- Helpful but not boring\n- Can roast people lightly when appropriate\n- Enthusiastic about cool tech and projects\n- Supportive of beginners learning to code\n- Concise - this is Discord, not an essay\n\n⚠️ CRITICAL RULES:\n- NEVER type @.everyone or @.here (remove the dots) - these ping hundreds of people\n- NEVER use mass mention pings under any circumstances\n- If you need to address the group, say \"everyone\" or \"folks\" without the @ symbol\n\nKeep responses under 2000 chars. Use Discord markdown when helpful.",
"channels": []
"channels": [],
"historyLength": 20,
"historyTTLDays": 30
},
"chimeIn": {
"enabled": false,
Expand Down
11 changes: 6 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "bill-bot",
"packageManager": "pnpm@10.29.2",
"packageManager": "pnpm@10.28.2",
"version": "1.0.0",
"description": "Volvox Discord bot - AI chat, welcome messages, and moderation",
"main": "src/index.js",
Expand All @@ -9,11 +9,12 @@
"start": "node src/index.js",
"dev": "node --watch src/index.js",
"deploy": "node src/deploy-commands.js",
"lint": "biome check .",
"lint:fix": "biome check . --fix",
"format": "biome format . --write",
"test": "vitest run",
"test:coverage": "vitest run --coverage"
"test:watch": "vitest",
"test:coverage": "vitest run --coverage",
"lint": "biome check .",
"lint:fix": "biome check . --write",
"format": "biome format . --write"
},
"dependencies": {
"discord.js": "^14.25.1",
Expand Down
23 changes: 22 additions & 1 deletion src/db.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,10 @@ function getSslConfig(connectionString) {
* @returns {Promise<pg.Pool>} The connection pool
*/
export async function initDb() {
if (pool) return pool;
if (initializing) {
throw new Error('initDb is already in progress');
}
if (pool) return pool;

initializing = true;
try {
Expand Down Expand Up @@ -96,6 +96,27 @@ export async function initDb() {
)
`);

await pool.query(`
CREATE TABLE IF NOT EXISTS conversations (
id SERIAL PRIMARY KEY,
channel_id TEXT NOT NULL,
role TEXT NOT NULL CHECK (role IN ('user', 'assistant', 'system')),
content TEXT NOT NULL,
username TEXT,
created_at TIMESTAMPTZ DEFAULT NOW()
)
`);

await pool.query(`
CREATE INDEX IF NOT EXISTS idx_conversations_channel_created
ON conversations (channel_id, created_at)
`);

await pool.query(`
CREATE INDEX IF NOT EXISTS idx_conversations_created_at
ON conversations (created_at)
`);

info('Database schema initialized');
} catch (err) {
// Clean up the pool so getPool() doesn't return an unusable instance
Expand Down
39 changes: 31 additions & 8 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,14 @@ import { Client, Collection, Events, GatewayIntentBits } from 'discord.js';
import { config as dotenvConfig } from 'dotenv';
import { closeDb, initDb } from './db.js';
import { error, info, warn } from './logger.js';
import { getConversationHistory, setConversationHistory } from './modules/ai.js';
import {
getConversationHistory,
initConversationHistory,
setConversationHistory,
setPool,
startConversationCleanup,
stopConversationCleanup,
} from './modules/ai.js';
import { loadConfig } from './modules/config.js';
import { registerEventHandlers } from './modules/events.js';
import { HealthMonitor } from './utils/health.js';
Expand Down Expand Up @@ -234,23 +241,26 @@ async function gracefulShutdown(signal) {
}
}

// 2. Save state after pending requests complete
// 2. Stop conversation cleanup timer
stopConversationCleanup();

// 3. Save state after pending requests complete
info('Saving conversation state');
saveState();

// 3. Close database pool
// 4. Close database pool
info('Closing database connection');
try {
await closeDb();
} catch (err) {
error('Failed to close database pool', { error: err.message });
}

// 4. Destroy Discord client
// 5. Destroy Discord client
info('Disconnecting from Discord');
client.destroy();

// 5. Log clean exit
// 6. Log clean exit
info('Shutdown complete');
process.exit(0);
}
Expand All @@ -275,7 +285,6 @@ process.on('unhandledRejection', (err) => {
type: typeof err,
});
});

// Start bot
const token = process.env.DISCORD_TOKEN;
if (!token) {
Expand All @@ -294,8 +303,9 @@ if (!token) {
*/
async function startup() {
// Initialize database
let dbPool = null;
if (process.env.DATABASE_URL) {
await initDb();
dbPool = await initDb();
info('Database initialized');
} else {
warn('DATABASE_URL not set — using config.json only (no persistence)');
Expand All @@ -305,9 +315,22 @@ async function startup() {
config = await loadConfig();
info('Configuration loaded', { sections: Object.keys(config) });

// Load previous conversation state
// Set up AI module's DB pool reference
if (dbPool) {
setPool(dbPool);
}

// TODO: loadState() is migration-only for file->DB persistence transition.
// When DB is available, initConversationHistory() effectively overwrites this state.
// Once all environments are DB-backed, remove this call and loadState/saveState helpers.
loadState();

// Hydrate conversation history from DB (overwrites file state if DB is available)
await initConversationHistory();

// Start periodic conversation cleanup
startConversationCleanup();

// Register event handlers with live config reference
registerEventHandlers(client, config, healthMonitor);

Expand Down
Loading
Loading