refactor(db): unify SQLite table schema management and initialization (#310)
Centralized all 11 Hermes SQLite table definitions and initialization logic into a single schemas.ts file to eliminate duplication and improve maintainability. Changes: - **NEW**: packages/server/src/db/hermes/schemas.ts - Centralized schema definitions for all 11 tables - Unified initAllHermesTables() function with migration logic - Includes usage table PRIMARY KEY migration (session_id → id) - **Refactored**: packages/server/src/db/hermes/init.ts - Simplified from async to sync (all operations are synchronous) - Single responsibility: delegate to schemas.ts - **Refactored**: packages/server/src/db/hermes/session-store.ts - Removed schema definitions (now in schemas.ts) - Removed initSessionStore() function - Imports table constants from schemas.ts - **Refactored**: packages/server/src/db/hermes/usage-store.ts - Removed initUsageStore() function and migration logic - Migration moved to schemas.ts for consistency - Only handles CRUD operations now - **Refactored**: packages/server/src/db/hermes/compression-snapshot.ts - Removed initCompressionSnapshotStore() function - Fixed duplicate getCompressionSnapshot definition - Imports table constant from schemas.ts - **Refactored**: packages/server/src/services/hermes/group-chat/index.ts - Removed ensureTable() calls (now in schemas.ts) - Only handles index creation now - Imports table constants from schemas.ts - **Updated**: packages/server/src/index.ts - Removed await from initAllStores() call (now sync) Benefits: - 🎯 Single responsibility: schemas.ts manages all tables, stores only do CRUD - 📋 Centralized maintenance: all table definitions in one place - 🔄 No duplication: each table created exactly once with proper migrations - 🚀 Clean architecture: clear separation between initialization and operations Tables managed (11 total): 1. session_usage (usage statistics) 2. sessions (session metadata) 3. messages (message content) 4. chat_compression_snapshots (compression snapshots) 5. gc_rooms (group chat rooms) 6. gc_messages (group chat messages) 7. gc_room_agents (room agents) 8. gc_context_snapshots (group chat snapshots) 9. gc_room_members (room members) 10. gc_pending_session_deletes (pending session deletes) 11. gc_session_profiles (session profiles) Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -6,23 +6,8 @@
|
||||
* the previous one left off.
|
||||
*/
|
||||
|
||||
import { isSqliteAvailable, ensureTable, getDb } from '../index'
|
||||
|
||||
const TABLE = 'chat_compression_snapshots'
|
||||
|
||||
const SCHEMA: Record<string, string> = {
|
||||
session_id: 'TEXT PRIMARY KEY',
|
||||
summary: 'TEXT NOT NULL DEFAULT \'\'',
|
||||
last_message_index: 'INTEGER NOT NULL DEFAULT 0',
|
||||
message_count_at_time: 'INTEGER NOT NULL DEFAULT 0',
|
||||
updated_at: 'INTEGER NOT NULL',
|
||||
}
|
||||
|
||||
export function initCompressionSnapshotStore(): void {
|
||||
if (isSqliteAvailable()) {
|
||||
ensureTable(TABLE, SCHEMA)
|
||||
}
|
||||
}
|
||||
import { isSqliteAvailable, getDb } from '../index'
|
||||
import { COMPRESSION_SNAPSHOT_TABLE as TABLE } from './schemas'
|
||||
|
||||
export function getCompressionSnapshot(sessionId: string): { summary: string; lastMessageIndex: number; messageCountAtTime: number } | null {
|
||||
if (!isSqliteAvailable()) return null
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
/**
|
||||
* Unified initializer for all Hermes SQLite stores.
|
||||
* Call this once at bootstrap to create/migrate all tables.
|
||||
*
|
||||
* All table schemas, creation, and migration logic are now centralized
|
||||
* in schemas.ts to avoid duplication and ensure consistency.
|
||||
*/
|
||||
|
||||
export async function initAllStores(): Promise<void> {
|
||||
const { initUsageStore } = await import('./usage-store')
|
||||
initUsageStore()
|
||||
import { initAllHermesTables } from './schemas'
|
||||
|
||||
const { initSessionStore } = await import('./session-store')
|
||||
initSessionStore()
|
||||
|
||||
const { initCompressionSnapshotStore } = await import('./compression-snapshot')
|
||||
initCompressionSnapshotStore()
|
||||
export function initAllStores(): void {
|
||||
// Initialize all tables with centralized schema definitions and migrations
|
||||
initAllHermesTables()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,264 @@
|
||||
/**
|
||||
* Centralized schema definitions for all Hermes SQLite tables.
|
||||
* All table schemas are defined here for unified management and migration.
|
||||
*/
|
||||
|
||||
// ============================================================================
|
||||
// Usage Store (usage-store.ts)
|
||||
// ============================================================================
|
||||
|
||||
export const USAGE_TABLE = 'session_usage'
|
||||
|
||||
export const USAGE_SCHEMA: Record<string, string> = {
|
||||
id: 'INTEGER PRIMARY KEY AUTOINCREMENT',
|
||||
session_id: 'TEXT NOT NULL',
|
||||
input_tokens: 'INTEGER NOT NULL DEFAULT 0',
|
||||
output_tokens: 'INTEGER NOT NULL DEFAULT 0',
|
||||
cache_read_tokens: 'INTEGER NOT NULL DEFAULT 0',
|
||||
cache_write_tokens: 'INTEGER NOT NULL DEFAULT 0',
|
||||
reasoning_tokens: 'INTEGER NOT NULL DEFAULT 0',
|
||||
model: "TEXT NOT NULL DEFAULT ''",
|
||||
profile: "TEXT NOT NULL DEFAULT 'default'",
|
||||
created_at: 'INTEGER NOT NULL',
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Session Store (session-store.ts)
|
||||
// ============================================================================
|
||||
|
||||
export const SESSIONS_TABLE = 'sessions'
|
||||
|
||||
export const SESSIONS_SCHEMA: Record<string, string> = {
|
||||
id: 'TEXT PRIMARY KEY',
|
||||
profile: 'TEXT NOT NULL DEFAULT \'default\'',
|
||||
source: 'TEXT NOT NULL DEFAULT \'api_server\'',
|
||||
user_id: 'TEXT',
|
||||
model: 'TEXT NOT NULL DEFAULT \'\'',
|
||||
title: 'TEXT',
|
||||
started_at: 'INTEGER NOT NULL',
|
||||
ended_at: 'INTEGER',
|
||||
end_reason: 'TEXT',
|
||||
message_count: 'INTEGER NOT NULL DEFAULT 0',
|
||||
tool_call_count: 'INTEGER NOT NULL DEFAULT 0',
|
||||
input_tokens: 'INTEGER NOT NULL DEFAULT 0',
|
||||
output_tokens: 'INTEGER NOT NULL DEFAULT 0',
|
||||
cache_read_tokens: 'INTEGER NOT NULL DEFAULT 0',
|
||||
cache_write_tokens: 'INTEGER NOT NULL DEFAULT 0',
|
||||
reasoning_tokens: 'INTEGER NOT NULL DEFAULT 0',
|
||||
billing_provider: 'TEXT',
|
||||
estimated_cost_usd: 'REAL NOT NULL DEFAULT 0',
|
||||
actual_cost_usd: 'REAL',
|
||||
cost_status: 'TEXT NOT NULL DEFAULT \'\'',
|
||||
preview: 'TEXT NOT NULL DEFAULT \'\'',
|
||||
last_active: 'INTEGER NOT NULL',
|
||||
}
|
||||
|
||||
export const MESSAGES_TABLE = 'messages'
|
||||
|
||||
export const MESSAGES_SCHEMA: Record<string, string> = {
|
||||
id: 'INTEGER PRIMARY KEY AUTOINCREMENT',
|
||||
session_id: 'TEXT NOT NULL',
|
||||
role: 'TEXT NOT NULL',
|
||||
content: 'TEXT NOT NULL DEFAULT \'\'',
|
||||
tool_call_id: 'TEXT',
|
||||
tool_calls: 'TEXT',
|
||||
tool_name: 'TEXT',
|
||||
timestamp: 'INTEGER NOT NULL',
|
||||
token_count: 'INTEGER',
|
||||
finish_reason: 'TEXT',
|
||||
reasoning: 'TEXT',
|
||||
reasoning_details: 'TEXT',
|
||||
reasoning_content: 'TEXT',
|
||||
codex_reasoning_items: 'TEXT',
|
||||
}
|
||||
|
||||
export const MESSAGES_INDEX = 'CREATE INDEX IF NOT EXISTS idx_messages_session_id ON messages(session_id)'
|
||||
|
||||
// ============================================================================
|
||||
// Compression Snapshot (compression-snapshot.ts)
|
||||
// ============================================================================
|
||||
|
||||
export const COMPRESSION_SNAPSHOT_TABLE = 'chat_compression_snapshots'
|
||||
|
||||
export const COMPRESSION_SNAPSHOT_SCHEMA: Record<string, string> = {
|
||||
session_id: 'TEXT PRIMARY KEY',
|
||||
summary: 'TEXT NOT NULL DEFAULT \'\'',
|
||||
last_message_index: 'INTEGER NOT NULL DEFAULT 0',
|
||||
message_count_at_time: 'INTEGER NOT NULL DEFAULT 0',
|
||||
updated_at: 'INTEGER NOT NULL',
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Group Chat (services/hermes/group-chat/index.ts)
|
||||
// ============================================================================
|
||||
|
||||
export const GC_ROOMS_TABLE = 'gc_rooms'
|
||||
|
||||
export const GC_ROOMS_SCHEMA: Record<string, string> = {
|
||||
id: 'TEXT PRIMARY KEY',
|
||||
name: 'TEXT NOT NULL',
|
||||
inviteCode: 'TEXT UNIQUE',
|
||||
triggerTokens: 'INTEGER NOT NULL DEFAULT 100000',
|
||||
maxHistoryTokens: 'INTEGER NOT NULL DEFAULT 32000',
|
||||
tailMessageCount: 'INTEGER NOT NULL DEFAULT 20',
|
||||
totalTokens: 'INTEGER NOT NULL DEFAULT 0',
|
||||
}
|
||||
|
||||
export const GC_MESSAGES_TABLE = 'gc_messages'
|
||||
|
||||
export const GC_MESSAGES_SCHEMA: Record<string, string> = {
|
||||
id: 'TEXT PRIMARY KEY',
|
||||
roomId: 'TEXT NOT NULL',
|
||||
senderId: 'TEXT NOT NULL',
|
||||
senderName: 'TEXT NOT NULL',
|
||||
content: 'TEXT NOT NULL',
|
||||
timestamp: 'INTEGER NOT NULL',
|
||||
}
|
||||
|
||||
export const GC_ROOM_AGENTS_TABLE = 'gc_room_agents'
|
||||
|
||||
export const GC_ROOM_AGENTS_SCHEMA: Record<string, string> = {
|
||||
id: 'TEXT PRIMARY KEY',
|
||||
roomId: 'TEXT NOT NULL',
|
||||
agentId: 'TEXT NOT NULL',
|
||||
profile: 'TEXT NOT NULL',
|
||||
name: 'TEXT NOT NULL',
|
||||
description: "TEXT NOT NULL DEFAULT ''",
|
||||
invited: 'INTEGER NOT NULL DEFAULT 0',
|
||||
}
|
||||
|
||||
export const GC_CONTEXT_SNAPSHOTS_TABLE = 'gc_context_snapshots'
|
||||
|
||||
export const GC_CONTEXT_SNAPSHOTS_SCHEMA: Record<string, string> = {
|
||||
roomId: 'TEXT PRIMARY KEY',
|
||||
summary: 'TEXT NOT NULL DEFAULT \'\'',
|
||||
lastMessageId: 'TEXT NOT NULL',
|
||||
lastMessageTimestamp: 'INTEGER NOT NULL',
|
||||
updatedAt: 'INTEGER NOT NULL',
|
||||
}
|
||||
|
||||
export const GC_ROOM_MEMBERS_TABLE = 'gc_room_members'
|
||||
|
||||
export const GC_ROOM_MEMBERS_SCHEMA: Record<string, string> = {
|
||||
id: 'TEXT PRIMARY KEY',
|
||||
roomId: 'TEXT NOT NULL',
|
||||
userId: 'TEXT NOT NULL',
|
||||
userName: 'TEXT NOT NULL',
|
||||
description: "TEXT NOT NULL DEFAULT ''",
|
||||
joinedAt: 'INTEGER NOT NULL',
|
||||
updatedAt: 'INTEGER NOT NULL',
|
||||
}
|
||||
|
||||
export const GC_PENDING_SESSION_DELETES_TABLE = 'gc_pending_session_deletes'
|
||||
|
||||
export const GC_PENDING_SESSION_DELETES_SCHEMA: Record<string, string> = {
|
||||
session_id: 'TEXT PRIMARY KEY',
|
||||
profile_name: 'TEXT NOT NULL',
|
||||
status: "TEXT NOT NULL DEFAULT 'pending'",
|
||||
attempt_count: 'INTEGER NOT NULL DEFAULT 0',
|
||||
last_error: 'TEXT',
|
||||
created_at: 'INTEGER NOT NULL',
|
||||
updated_at: 'INTEGER NOT NULL',
|
||||
next_attempt_at: 'INTEGER NOT NULL DEFAULT 0',
|
||||
}
|
||||
|
||||
export const GC_SESSION_PROFILES_TABLE = 'gc_session_profiles'
|
||||
|
||||
export const GC_SESSION_PROFILES_SCHEMA: Record<string, string> = {
|
||||
session_id: 'TEXT PRIMARY KEY',
|
||||
room_id: 'TEXT NOT NULL',
|
||||
agent_id: 'TEXT NOT NULL',
|
||||
profile_name: 'TEXT NOT NULL',
|
||||
created_at: 'INTEGER NOT NULL',
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Unified Initializer
|
||||
// ============================================================================
|
||||
|
||||
import { ensureTable, getDb } from '../index'
|
||||
|
||||
/**
|
||||
* Initialize all Hermes SQLite tables with proper schemas.
|
||||
* This function creates tables and adds missing columns if schemas change.
|
||||
* Call this once at application bootstrap.
|
||||
*/
|
||||
export function initAllHermesTables(): void {
|
||||
const db = getDb()
|
||||
if (!db) return
|
||||
|
||||
// Usage store - with special migration logic
|
||||
const tableExists = db.prepare(`SELECT name FROM sqlite_master WHERE type='table' AND name=?`).get(USAGE_TABLE)
|
||||
const cols = (tableExists
|
||||
? db.prepare(`PRAGMA table_info("${USAGE_TABLE}")`).all() as Array<{ name: string; pk: number }>
|
||||
: [])
|
||||
const hasId = cols.some(c => c.name === 'id')
|
||||
if (!hasId && tableExists) {
|
||||
// Migration: if session_id is still PRIMARY KEY (no separate id column), recreate table
|
||||
const oldCols = new Set(cols.map(c => c.name))
|
||||
const insertCols = ['session_id', 'input_tokens', 'output_tokens']
|
||||
const selectCols = [...insertCols]
|
||||
if (oldCols.has('cache_read_tokens')) { insertCols.push('cache_read_tokens'); selectCols.push('cache_read_tokens') }
|
||||
if (oldCols.has('cache_write_tokens')) { insertCols.push('cache_write_tokens'); selectCols.push('cache_write_tokens') }
|
||||
if (oldCols.has('reasoning_tokens')) { insertCols.push('reasoning_tokens'); selectCols.push('reasoning_tokens') }
|
||||
if (oldCols.has('created_at')) { insertCols.push('created_at'); selectCols.push('created_at') }
|
||||
if (oldCols.has('model')) { insertCols.push('model'); selectCols.push('model') }
|
||||
const defaults = {
|
||||
cache_read_tokens: 0, cache_write_tokens: 0, reasoning_tokens: 0,
|
||||
created_at: Date.now(), model: '', profile: 'default',
|
||||
}
|
||||
const insertValues = insertCols.map(c => c)
|
||||
const selectValues = selectCols.map(c => c)
|
||||
// Columns in new schema but not in old table — use defaults
|
||||
for (const [col] of Object.entries(USAGE_SCHEMA)) {
|
||||
if (!oldCols.has(col) && col !== 'id') {
|
||||
insertValues.push(col)
|
||||
selectValues.push(String(defaults[col as keyof typeof defaults] ?? 0))
|
||||
}
|
||||
}
|
||||
db.exec(`ALTER TABLE "${USAGE_TABLE}" RENAME TO "${USAGE_TABLE}_old"`)
|
||||
db.exec(`CREATE TABLE "${USAGE_TABLE}" (${Object.entries(USAGE_SCHEMA).map(([col, def]) => `"${col}" ${def}`).join(', ')})`)
|
||||
db.exec(`INSERT INTO "${USAGE_TABLE}" (${insertValues.join(', ')}) SELECT ${selectValues.join(', ')} FROM "${USAGE_TABLE}_old"`)
|
||||
db.exec(`DROP TABLE "${USAGE_TABLE}_old"`)
|
||||
}
|
||||
ensureTable(USAGE_TABLE, USAGE_SCHEMA)
|
||||
|
||||
// Session store
|
||||
ensureTable(SESSIONS_TABLE, SESSIONS_SCHEMA)
|
||||
ensureTable(MESSAGES_TABLE, MESSAGES_SCHEMA)
|
||||
db.exec(MESSAGES_INDEX)
|
||||
|
||||
// Compression snapshot
|
||||
ensureTable(COMPRESSION_SNAPSHOT_TABLE, COMPRESSION_SNAPSHOT_SCHEMA)
|
||||
|
||||
// Group chat - basic tables
|
||||
ensureTable(GC_ROOMS_TABLE, GC_ROOMS_SCHEMA)
|
||||
ensureTable(GC_MESSAGES_TABLE, GC_MESSAGES_SCHEMA)
|
||||
ensureTable(GC_CONTEXT_SNAPSHOTS_TABLE, GC_CONTEXT_SNAPSHOTS_SCHEMA)
|
||||
ensureTable(GC_PENDING_SESSION_DELETES_TABLE, GC_PENDING_SESSION_DELETES_SCHEMA)
|
||||
ensureTable(GC_SESSION_PROFILES_TABLE, GC_SESSION_PROFILES_SCHEMA)
|
||||
|
||||
// Group chat - composite primary key tables
|
||||
// Create without PK first, then add PK constraint
|
||||
ensureTable(GC_ROOM_AGENTS_TABLE, GC_ROOM_AGENTS_SCHEMA)
|
||||
ensureTable(GC_ROOM_MEMBERS_TABLE, GC_ROOM_MEMBERS_SCHEMA)
|
||||
|
||||
// Add composite primary keys (SQLite doesn't support ADD PK, so we recreate if needed)
|
||||
try {
|
||||
db.exec(`CREATE TABLE IF NOT EXISTS ${GC_ROOM_AGENTS_TABLE}_new (${Object.entries(GC_ROOM_AGENTS_SCHEMA).map(([k, v]) => `"${k}" ${v}`).join(', ')}, PRIMARY KEY (room_id, agent_id))`)
|
||||
db.exec(`INSERT OR IGNORE INTO ${GC_ROOM_AGENTS_TABLE}_new SELECT * FROM ${GC_ROOM_AGENTS_TABLE}`)
|
||||
db.exec(`DROP TABLE IF EXISTS ${GC_ROOM_AGENTS_TABLE}`)
|
||||
db.exec(`ALTER TABLE ${GC_ROOM_AGENTS_TABLE}_new RENAME TO ${GC_ROOM_AGENTS_TABLE}`)
|
||||
} catch {
|
||||
// Table already has correct schema or migration failed
|
||||
}
|
||||
|
||||
try {
|
||||
db.exec(`CREATE TABLE IF NOT EXISTS ${GC_ROOM_MEMBERS_TABLE}_new (${Object.entries(GC_ROOM_MEMBERS_SCHEMA).map(([k, v]) => `"${k}" ${v}`).join(', ')}, PRIMARY KEY (room_id, user_id))`)
|
||||
db.exec(`INSERT OR IGNORE INTO ${GC_ROOM_MEMBERS_TABLE}_new SELECT * FROM ${GC_ROOM_MEMBERS_TABLE}`)
|
||||
db.exec(`DROP TABLE IF EXISTS ${GC_ROOM_MEMBERS_TABLE}`)
|
||||
db.exec(`ALTER TABLE ${GC_ROOM_MEMBERS_TABLE}_new RENAME TO ${GC_ROOM_MEMBERS_TABLE}`)
|
||||
} catch {
|
||||
// Table already has correct schema or migration failed
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,8 @@
|
||||
* Self-built session database — completely replaces Hermes CLI dependency.
|
||||
* Uses the same ensureTable/getDb pattern as usage-store.ts.
|
||||
*/
|
||||
import { isSqliteAvailable, ensureTable, getDb } from '../index'
|
||||
import { isSqliteAvailable, getDb } from '../index'
|
||||
import { SESSIONS_TABLE, MESSAGES_TABLE } from './schemas'
|
||||
|
||||
// Re-export types for compatibility with sessions-db.ts consumers
|
||||
export interface HermesSessionRow {
|
||||
@@ -57,65 +58,8 @@ export interface HermesSessionDetailRow extends HermesSessionRow {
|
||||
thread_session_count: number
|
||||
}
|
||||
|
||||
// --- Schema ---
|
||||
|
||||
const SESSIONS_TABLE = 'sessions'
|
||||
|
||||
const SESSIONS_SCHEMA: Record<string, string> = {
|
||||
id: 'TEXT PRIMARY KEY',
|
||||
profile: 'TEXT NOT NULL DEFAULT \'default\'',
|
||||
source: 'TEXT NOT NULL DEFAULT \'api_server\'',
|
||||
user_id: 'TEXT',
|
||||
model: 'TEXT NOT NULL DEFAULT \'\'',
|
||||
title: 'TEXT',
|
||||
started_at: 'INTEGER NOT NULL',
|
||||
ended_at: 'INTEGER',
|
||||
end_reason: 'TEXT',
|
||||
message_count: 'INTEGER NOT NULL DEFAULT 0',
|
||||
tool_call_count: 'INTEGER NOT NULL DEFAULT 0',
|
||||
input_tokens: 'INTEGER NOT NULL DEFAULT 0',
|
||||
output_tokens: 'INTEGER NOT NULL DEFAULT 0',
|
||||
cache_read_tokens: 'INTEGER NOT NULL DEFAULT 0',
|
||||
cache_write_tokens: 'INTEGER NOT NULL DEFAULT 0',
|
||||
reasoning_tokens: 'INTEGER NOT NULL DEFAULT 0',
|
||||
billing_provider: 'TEXT',
|
||||
estimated_cost_usd: 'REAL NOT NULL DEFAULT 0',
|
||||
actual_cost_usd: 'REAL',
|
||||
cost_status: 'TEXT NOT NULL DEFAULT \'\'',
|
||||
preview: 'TEXT NOT NULL DEFAULT \'\'',
|
||||
last_active: 'INTEGER NOT NULL',
|
||||
}
|
||||
|
||||
const MESSAGES_TABLE = 'messages'
|
||||
|
||||
const MESSAGES_SCHEMA: Record<string, string> = {
|
||||
id: 'INTEGER PRIMARY KEY AUTOINCREMENT',
|
||||
session_id: 'TEXT NOT NULL',
|
||||
role: 'TEXT NOT NULL',
|
||||
content: 'TEXT NOT NULL DEFAULT \'\'',
|
||||
tool_call_id: 'TEXT',
|
||||
tool_calls: 'TEXT',
|
||||
tool_name: 'TEXT',
|
||||
timestamp: 'INTEGER NOT NULL',
|
||||
token_count: 'INTEGER',
|
||||
finish_reason: 'TEXT',
|
||||
reasoning: 'TEXT',
|
||||
reasoning_details: 'TEXT',
|
||||
reasoning_content: 'TEXT',
|
||||
codex_reasoning_items: 'TEXT',
|
||||
}
|
||||
|
||||
const MESSAGES_INDEX = 'CREATE INDEX IF NOT EXISTS idx_messages_session_id ON messages(session_id)'
|
||||
|
||||
// --- Init ---
|
||||
|
||||
export function initSessionStore(): void {
|
||||
if (!isSqliteAvailable()) return
|
||||
ensureTable(SESSIONS_TABLE, SESSIONS_SCHEMA)
|
||||
ensureTable(MESSAGES_TABLE, MESSAGES_SCHEMA)
|
||||
const db = getDb()!
|
||||
db.exec(MESSAGES_INDEX)
|
||||
}
|
||||
// Note: Table schemas and initialization are now centralized in schemas.ts
|
||||
// Tables are created automatically on bootstrap via initAllHermesTables()
|
||||
|
||||
// --- Helpers ---
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { isSqliteAvailable, ensureTable, getDb, jsonSet, jsonGet, jsonGetAll, jsonDelete } from '../index'
|
||||
|
||||
const TABLE = 'session_usage'
|
||||
import { isSqliteAvailable, getDb, jsonSet, jsonGet, jsonGetAll, jsonDelete } from '../index'
|
||||
import { USAGE_TABLE as TABLE } from './schemas'
|
||||
|
||||
export interface UsageRecord {
|
||||
input_tokens: number
|
||||
@@ -13,61 +12,6 @@ export interface UsageRecord {
|
||||
created_at: number
|
||||
}
|
||||
|
||||
const SCHEMA = {
|
||||
id: 'INTEGER PRIMARY KEY AUTOINCREMENT',
|
||||
session_id: 'TEXT NOT NULL',
|
||||
input_tokens: 'INTEGER NOT NULL DEFAULT 0',
|
||||
output_tokens: 'INTEGER NOT NULL DEFAULT 0',
|
||||
cache_read_tokens: 'INTEGER NOT NULL DEFAULT 0',
|
||||
cache_write_tokens: 'INTEGER NOT NULL DEFAULT 0',
|
||||
reasoning_tokens: 'INTEGER NOT NULL DEFAULT 0',
|
||||
model: "TEXT NOT NULL DEFAULT ''",
|
||||
profile: "TEXT NOT NULL DEFAULT 'default'",
|
||||
created_at: 'INTEGER NOT NULL',
|
||||
}
|
||||
|
||||
export function initUsageStore(): void {
|
||||
if (!isSqliteAvailable()) return
|
||||
const db = getDb()!
|
||||
|
||||
// Migration: if session_id is still PRIMARY KEY (no separate id column), recreate table
|
||||
// Must run BEFORE ensureTable, because ensureTable can't ALTER TABLE ADD a PRIMARY KEY column
|
||||
const tableExists = db.prepare(`SELECT name FROM sqlite_master WHERE type='table' AND name=?`).get(TABLE)
|
||||
const cols = (tableExists
|
||||
? db.prepare(`PRAGMA table_info("${TABLE}")`).all() as Array<{ name: string; pk: number }>
|
||||
: [])
|
||||
const hasId = cols.some(c => c.name === 'id')
|
||||
if (!hasId && tableExists) {
|
||||
const oldCols = new Set(cols.map(c => c.name))
|
||||
const insertCols = ['session_id', 'input_tokens', 'output_tokens']
|
||||
const selectCols = [...insertCols]
|
||||
if (oldCols.has('cache_read_tokens')) { insertCols.push('cache_read_tokens'); selectCols.push('cache_read_tokens') }
|
||||
if (oldCols.has('cache_write_tokens')) { insertCols.push('cache_write_tokens'); selectCols.push('cache_write_tokens') }
|
||||
if (oldCols.has('reasoning_tokens')) { insertCols.push('reasoning_tokens'); selectCols.push('reasoning_tokens') }
|
||||
if (oldCols.has('created_at')) { insertCols.push('created_at'); selectCols.push('created_at') }
|
||||
if (oldCols.has('model')) { insertCols.push('model'); selectCols.push('model') }
|
||||
const defaults = {
|
||||
cache_read_tokens: 0, cache_write_tokens: 0, reasoning_tokens: 0,
|
||||
created_at: Date.now(), model: '', profile: 'default',
|
||||
}
|
||||
const insertValues = insertCols.map(c => c)
|
||||
const selectValues = selectCols.map(c => c)
|
||||
// Columns in new schema but not in old table — use defaults
|
||||
for (const [col, def] of Object.entries(SCHEMA)) {
|
||||
if (!oldCols.has(col) && col !== 'id') {
|
||||
insertValues.push(col)
|
||||
selectValues.push(String(defaults[col as keyof typeof defaults] ?? 0))
|
||||
}
|
||||
}
|
||||
db.exec(`ALTER TABLE "${TABLE}" RENAME TO "${TABLE}_old"`)
|
||||
db.exec(`CREATE TABLE "${TABLE}" (${Object.entries(SCHEMA).map(([col, def]) => `"${col}" ${def}`).join(', ')})`)
|
||||
db.exec(`INSERT INTO "${TABLE}" (${insertValues.join(', ')}) SELECT ${selectValues.join(', ')} FROM "${TABLE}_old"`)
|
||||
db.exec(`DROP TABLE "${TABLE}_old"`)
|
||||
}
|
||||
|
||||
ensureTable(TABLE, SCHEMA)
|
||||
}
|
||||
|
||||
export function updateUsage(
|
||||
sessionId: string,
|
||||
data: {
|
||||
|
||||
@@ -51,7 +51,7 @@ export async function bootstrap() {
|
||||
|
||||
// Initialize all web-ui SQLite tables
|
||||
const { initAllStores } = await import('./db/hermes/init')
|
||||
await initAllStores()
|
||||
initAllStores()
|
||||
console.log('[bootstrap] all stores initialized')
|
||||
|
||||
// Sync Hermes sessions from all profiles (only if local DB is empty)
|
||||
|
||||
@@ -2,7 +2,8 @@ import { Server, Socket, Namespace } from 'socket.io'
|
||||
import type { Server as HttpServer } from 'http'
|
||||
import { getToken } from '../../../services/auth'
|
||||
import { logger } from '../../../services/logger'
|
||||
import { getDb, ensureTable } from '../../../db'
|
||||
import { getDb } from '../../../db'
|
||||
import { GC_ROOMS_TABLE, GC_MESSAGES_TABLE, GC_ROOM_AGENTS_TABLE, GC_CONTEXT_SNAPSHOTS_TABLE, GC_ROOM_MEMBERS_TABLE, GC_PENDING_SESSION_DELETES_TABLE, GC_SESSION_PROFILES_TABLE } from '../../../db/hermes/schemas'
|
||||
import { AgentClients } from './agent-clients'
|
||||
import { ContextEngine } from '../context-engine/compressor'
|
||||
import { SessionDeleter } from '../session-deleter'
|
||||
@@ -139,14 +140,8 @@ class ChatStorage {
|
||||
if (_tablesEnsured) return
|
||||
const db = this.db()
|
||||
if (!db) return
|
||||
ensureTable('gc_rooms', GC_ROOMS_SCHEMA)
|
||||
ensureTable('gc_messages', GC_MESSAGES_SCHEMA)
|
||||
ensureTable('gc_room_agents', GC_ROOM_AGENTS_SCHEMA)
|
||||
ensureTable('gc_context_snapshots', GC_CONTEXT_SNAPSHOTS_SCHEMA)
|
||||
ensureTable('gc_room_members', GC_ROOM_MEMBERS_SCHEMA)
|
||||
ensureTable('gc_pending_session_deletes', GC_PENDING_SESSION_DELETES_SCHEMA)
|
||||
ensureTable('gc_session_profiles', GC_SESSION_PROFILES_SCHEMA)
|
||||
// Indexes (safe to run multiple times — CREATE INDEX IF NOT EXISTS)
|
||||
// Tables are now created centrally in initAllHermesTables()
|
||||
// Only create indexes here
|
||||
try { db.exec('CREATE INDEX IF NOT EXISTS idx_gc_messages_room ON gc_messages(roomId, timestamp)') } catch { /* ignore */ }
|
||||
try { db.exec('CREATE INDEX IF NOT EXISTS idx_gc_room_agents_room ON gc_room_agents(roomId)') } catch { /* ignore */ }
|
||||
try { db.exec('CREATE UNIQUE INDEX IF NOT EXISTS idx_gc_room_members_unique ON gc_room_members(roomId, userId)') } catch { /* ignore */ }
|
||||
|
||||
Reference in New Issue
Block a user