sessions.ts
1 import { randomUUID } from 'node:crypto' 2 3 import type { SessionSnapshot } from '@/lib/shared/chat' 4 import { getAppSettings } from '@/server/storage/app-settings' 5 import { getDb } from '@/server/storage/db' 6 import { getMessages, getMessageCount } from './messages' 7 import { listMemories } from './memories' 8 import type { 9 SessionListItem, 10 SessionListRow, 11 SessionRow, 12 } from './types' 13 14 export function ensureSession(sessionId?: string): SessionRow { 15 const db = getDb() 16 const now = new Date().toISOString() 17 const id = sessionId?.trim() || randomUUID() 18 19 const existing = db 20 .prepare( 21 `SELECT id, created_at, updated_at, compacted_summary, chat_model_id_override, thinking_level, reasoning_level 22 FROM sessions WHERE id = ?`, 23 ) 24 .get(id) as SessionRow | undefined 25 26 if (existing) { 27 return existing 28 } 29 30 const defaults = getAppSettings() 31 const seededChatModel = null 32 33 db.prepare( 34 `INSERT INTO sessions ( 35 id, created_at, updated_at, compacted_summary, chat_model_id_override, thinking_level, reasoning_level 36 ) VALUES (?, ?, ?, ?, ?, ?, ?)`, 37 ).run( 38 id, 39 now, 40 now, 41 '', 42 seededChatModel, 43 defaults.thinkingLevel, 44 defaults.reasoningLevel, 45 ) 46 47 return { 48 id, 49 created_at: now, 50 updated_at: now, 51 compacted_summary: '', 52 chat_model_id_override: seededChatModel, 53 thinking_level: defaults.thinkingLevel, 54 reasoning_level: defaults.reasoningLevel, 55 used_capabilities_json: '[]', 56 } 57 } 58 59 export function getSessionSnapshot( 60 sessionId: string, 61 options?: { messageLimit?: number }, 62 ): SessionSnapshot | null { 63 const db = getDb() 64 const session = db 65 .prepare( 66 'SELECT id, created_at, updated_at, compacted_summary, used_capabilities_json FROM sessions WHERE id = ?', 67 ) 68 .get(sessionId) as SessionRow | undefined 69 70 if (!session) return null 71 72 let usedCapabilities: string[] = [] 73 try { 74 usedCapabilities = session.used_capabilities_json 75 ? JSON.parse(session.used_capabilities_json) 76 : [] 77 } catch { 78 usedCapabilities = [] 79 } 80 81 const messageLimit = options?.messageLimit 82 const messages = getMessages(sessionId, messageLimit != null ? { limit: messageLimit } : undefined) 83 const totalMessageCount = messageLimit != null ? getMessageCount(sessionId) : messages.length 84 85 return { 86 sessionId: session.id, 87 createdAt: session.created_at, 88 updatedAt: session.updated_at, 89 compactedSummary: session.compacted_summary, 90 messages, 91 memories: listMemories(sessionId), 92 usedCapabilities, 93 totalMessageCount, 94 hasMoreMessages: messages.length < totalMessageCount, 95 } 96 } 97 98 export function listSessions(limit = 20, search?: string): SessionListItem[] { 99 const db = getDb() 100 const cappedLimit = Math.min(Math.max(limit, 1), 100) 101 const searchTerm = search?.trim() ? `%${search.trim()}%` : null 102 103 const rows = db 104 .prepare( 105 `SELECT 106 s.id, 107 s.created_at, 108 s.updated_at, 109 s.compacted_summary, 110 s.chat_model_id_override, 111 s.thinking_level, 112 s.reasoning_level, 113 COALESCE(( 114 SELECT COUNT(*) 115 FROM messages m 116 WHERE m.session_id = s.id 117 ), 0) AS message_count, 118 ( 119 SELECT m2.text 120 FROM messages m2 121 WHERE m2.session_id = s.id 122 ORDER BY m2.created_at DESC 123 LIMIT 1 124 ) AS last_message_text, 125 ( 126 SELECT m2.role 127 FROM messages m2 128 WHERE m2.session_id = s.id 129 ORDER BY m2.created_at DESC 130 LIMIT 1 131 ) AS last_message_role 132 FROM sessions s 133 WHERE (? IS NULL OR s.id LIKE ? OR ( 134 SELECT m3.text 135 FROM messages m3 136 WHERE m3.session_id = s.id 137 AND m3.text LIKE ? 138 LIMIT 1 139 ) IS NOT NULL) 140 ORDER BY s.updated_at DESC 141 LIMIT ?`, 142 ) 143 .all(searchTerm, searchTerm, searchTerm, cappedLimit) as SessionListRow[] 144 145 return rows.map((row) => ({ 146 id: row.id, 147 createdAt: row.created_at, 148 updatedAt: row.updated_at, 149 compactedSummary: row.compacted_summary, 150 chatModelOverride: row.chat_model_id_override, 151 thinkingLevel: row.thinking_level, 152 reasoningLevel: row.reasoning_level, 153 messageCount: row.message_count, 154 lastMessageText: row.last_message_text, 155 lastMessageRole: row.last_message_role, 156 })) 157 } 158 159 export function deleteSession(sessionId: string): void { 160 const db = getDb() 161 db.transaction(() => { 162 db.prepare('DELETE FROM messages WHERE session_id = ?').run(sessionId) 163 db.prepare('DELETE FROM memories WHERE session_id = ?').run(sessionId) 164 db.prepare('DELETE FROM memory_embeddings WHERE session_id = ?').run(sessionId) 165 db.prepare('DELETE FROM uploads WHERE session_id = ?').run(sessionId) 166 db.prepare('DELETE FROM sessions WHERE id = ?').run(sessionId) 167 })() 168 } 169 170 export function getSessionChatModelOverride(sessionId: string): string | null { 171 const db = getDb() 172 const row = db 173 .prepare('SELECT chat_model_id_override FROM sessions WHERE id = ?') 174 .get(sessionId) as Pick<SessionRow, 'chat_model_id_override'> | undefined 175 176 return row?.chat_model_id_override ?? null 177 } 178 179 export function setSessionChatModelOverride( 180 sessionId: string, 181 model: string | null, 182 ): void { 183 const db = getDb() 184 const normalized = model?.trim() ? model.trim() : null 185 const now = new Date().toISOString() 186 db.prepare( 187 'UPDATE sessions SET chat_model_id_override = ?, updated_at = ? WHERE id = ?', 188 ).run(normalized, now, sessionId) 189 } 190 191 export function getSessionThinkingLevelOverride( 192 sessionId: string, 193 ): string | null { 194 const db = getDb() 195 const row = db 196 .prepare('SELECT thinking_level FROM sessions WHERE id = ?') 197 .get(sessionId) as { thinking_level: string | null } | undefined 198 199 return row?.thinking_level ?? null 200 } 201 202 export function setSessionThinkingLevelOverride( 203 sessionId: string, 204 thinkingLevel: string | null, 205 ): void { 206 const db = getDb() 207 const normalized = thinkingLevel?.trim() ? thinkingLevel.trim() : null 208 const now = new Date().toISOString() 209 db.prepare( 210 'UPDATE sessions SET thinking_level = ?, updated_at = ? WHERE id = ?', 211 ).run(normalized, now, sessionId) 212 } 213 214 export function getSessionReasoningLevelOverride( 215 sessionId: string, 216 ): string | null { 217 const db = getDb() 218 const row = db 219 .prepare('SELECT reasoning_level FROM sessions WHERE id = ?') 220 .get(sessionId) as { reasoning_level: string | null } | undefined 221 222 return row?.reasoning_level ?? null 223 } 224 225 export function setSessionReasoningLevelOverride( 226 sessionId: string, 227 reasoningLevel: string | null, 228 ): void { 229 const db = getDb() 230 const normalized = reasoningLevel?.trim() ? reasoningLevel.trim() : null 231 const now = new Date().toISOString() 232 db.prepare( 233 'UPDATE sessions SET reasoning_level = ?, updated_at = ? WHERE id = ?', 234 ).run(normalized, now, sessionId) 235 } 236 237 export function getSessionUsedCapabilities(sessionId: string): string[] { 238 const db = getDb() 239 const row = db 240 .prepare('SELECT used_capabilities_json FROM sessions WHERE id = ?') 241 .get(sessionId) as { used_capabilities_json: string } | undefined 242 243 if (!row?.used_capabilities_json) return [] 244 try { 245 return JSON.parse(row.used_capabilities_json) as string[] 246 } catch { 247 return [] 248 } 249 } 250 251 export function addSessionUsedCapability( 252 sessionId: string, 253 capability: string, 254 ): void { 255 const db = getDb() 256 const existing = getSessionUsedCapabilities(sessionId) 257 if (!existing.includes(capability)) { 258 const updated = [...existing, capability] 259 const now = new Date().toISOString() 260 db.prepare( 261 'UPDATE sessions SET used_capabilities_json = ?, updated_at = ? WHERE id = ?', 262 ).run(JSON.stringify(updated), now, sessionId) 263 } 264 } 265 266 export function touchSession( 267 sessionId: string, 268 updatedAt = new Date().toISOString(), 269 ): void { 270 const db = getDb() 271 db.prepare('UPDATE sessions SET updated_at = ? WHERE id = ?').run( 272 updatedAt, 273 sessionId, 274 ) 275 }