agentgui 1.0.833 → 1.0.834

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/db-queries.js CHANGED
@@ -1,1412 +1,92 @@
1
- import { createACPQueries } from '../acp-queries.js';
2
-
3
- export function createQueries(db, prep, generateId) {
4
- return {
5
- _db: db,
6
- createConversation(agentType, title = null, workingDirectory = null, model = null, subAgent = null) {
7
- const id = generateId('conv');
8
- const now = Date.now();
9
- const stmt = prep(
10
- `INSERT INTO conversations (id, agentId, agentType, title, created_at, updated_at, status, workingDirectory, model, subAgent) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
11
- );
12
- stmt.run(id, agentType, agentType, title, now, now, 'active', workingDirectory, model, subAgent);
13
-
14
- return {
15
- id,
16
- agentType,
17
- title,
18
- workingDirectory,
19
- model,
20
- subAgent,
21
- created_at: now,
22
- updated_at: now,
23
- status: 'active'
24
- };
25
- },
26
-
27
- getConversation(id) {
28
- const stmt = prep('SELECT * FROM conversations WHERE id = ?');
29
- return stmt.get(id);
30
- },
31
-
32
- getAllConversations() {
33
- const stmt = prep('SELECT * FROM conversations WHERE status != ? ORDER BY updated_at DESC');
34
- return stmt.all('deleted');
35
- },
36
-
37
- getConversationsList() {
38
- const stmt = prep(
39
- 'SELECT id, agentId, title, agentType, created_at, updated_at, messageCount, workingDirectory, isStreaming, model, subAgent, pinned, sortOrder, tags FROM conversations WHERE status NOT IN (?, ?) ORDER BY pinned DESC, sortOrder ASC, updated_at DESC'
40
- );
41
- return stmt.all('deleted', 'archived');
42
- },
43
-
44
- getConversations() {
45
- const stmt = prep('SELECT * FROM conversations WHERE status NOT IN (?, ?) ORDER BY pinned DESC, sortOrder ASC, updated_at DESC');
46
- return stmt.all('deleted', 'archived');
47
- },
48
-
49
- updateConversationSortOrder(id, sortOrder) {
50
- prep('UPDATE conversations SET sortOrder = ? WHERE id = ?').run(sortOrder, id);
51
- },
52
-
53
- getArchivedConversations() {
54
- const stmt = prep('SELECT id, agentId, title, agentType, created_at, updated_at, messageCount, workingDirectory, model, subAgent FROM conversations WHERE status = ? ORDER BY updated_at DESC');
55
- return stmt.all('archived');
56
- },
57
-
58
- archiveConversation(id) {
59
- const conv = this.getConversation(id);
60
- if (!conv) return null;
61
- prep('UPDATE conversations SET status = ?, updated_at = ? WHERE id = ?').run('archived', Date.now(), id);
62
- return this.getConversation(id);
63
- },
64
-
65
- restoreConversation(id) {
66
- const conv = this.getConversation(id);
67
- if (!conv) return null;
68
- prep('UPDATE conversations SET status = ?, updated_at = ? WHERE id = ?').run('active', Date.now(), id);
69
- return this.getConversation(id);
70
- },
71
-
72
- updateConversation(id, data) {
73
- const conv = this.getConversation(id);
74
- if (!conv) return null;
75
-
76
- const now = Date.now();
77
- const title = data.title !== undefined ? data.title : conv.title;
78
- const status = data.status !== undefined ? data.status : conv.status;
79
- const agentId = data.agentId !== undefined ? data.agentId : conv.agentId;
80
- const agentType = data.agentType !== undefined ? data.agentType : conv.agentType;
81
- const model = data.model !== undefined ? data.model : conv.model;
82
- const subAgent = data.subAgent !== undefined ? data.subAgent : conv.subAgent;
83
- const pinned = data.pinned !== undefined ? (data.pinned ? 1 : 0) : (conv.pinned || 0);
84
- const tags = data.tags !== undefined ? (Array.isArray(data.tags) ? data.tags.join(',') : data.tags) : (conv.tags || null);
85
-
86
- const stmt = prep(
87
- `UPDATE conversations SET title = ?, status = ?, agentId = ?, agentType = ?, model = ?, subAgent = ?, pinned = ?, tags = ?, updated_at = ? WHERE id = ?`
88
- );
89
- stmt.run(title, status, agentId, agentType, model, subAgent, pinned, tags, now, id);
90
-
91
- return {
92
- ...conv,
93
- title,
94
- status,
95
- agentId,
96
- agentType,
97
- model,
98
- subAgent,
99
- pinned,
100
- tags,
101
- updated_at: now
102
- };
103
- },
104
-
105
- setClaudeSessionId(conversationId, claudeSessionId, sessionId = null) {
106
- const stmt = prep('UPDATE conversations SET claudeSessionId = ?, updated_at = ? WHERE id = ?');
107
- stmt.run(claudeSessionId, Date.now(), conversationId);
108
- // Also track on the current AgentGUI session so we can clean up all sessions on delete
109
- if (sessionId) {
110
- prep('UPDATE sessions SET claudeSessionId = ? WHERE id = ?').run(claudeSessionId, sessionId);
111
- }
112
- },
113
-
114
- getClaudeSessionId(conversationId) {
115
- const stmt = prep('SELECT claudeSessionId FROM conversations WHERE id = ?');
116
- const row = stmt.get(conversationId);
117
- return row?.claudeSessionId || null;
118
- },
119
-
120
- setIsStreaming(conversationId, isStreaming) {
121
- const stmt = prep('UPDATE conversations SET isStreaming = ?, updated_at = ? WHERE id = ?');
122
- stmt.run(isStreaming ? 1 : 0, Date.now(), conversationId);
123
- },
124
-
125
- getIsStreaming(conversationId) {
126
- const stmt = prep('SELECT isStreaming FROM conversations WHERE id = ?');
127
- const row = stmt.get(conversationId);
128
- return row?.isStreaming === 1;
129
- },
130
-
131
- getStreamingConversations() {
132
- const stmt = prep('SELECT id, title, claudeSessionId, agentId, agentType, model, subAgent FROM conversations WHERE isStreaming = 1');
133
- return stmt.all();
134
- },
135
-
136
- getResumableConversations(withinMs = 600000) {
137
- // Get conversations with incomplete sessions that can be resumed.
138
- // Only includes sessions with status: active, pending, interrupted.
139
- // Excludes complete and error sessions - agents that finished should not be resumed.
140
- // Only resumes sessions that started within the last withinMs (default 10 minutes).
141
- const cutoff = Date.now() - withinMs;
142
- const stmt = prep(
143
- `SELECT DISTINCT c.id, c.title, c.claudeSessionId, c.agentId, c.agentType, c.workingDirectory, c.model, c.subAgent
144
- FROM conversations c
145
- WHERE EXISTS (
146
- SELECT 1 FROM sessions s
147
- WHERE s.conversationId = c.id
148
- AND s.status IN ('active', 'pending', 'interrupted')
149
- AND s.started_at > ?
150
- )`
151
- );
152
- return stmt.all(cutoff);
153
- },
154
-
155
- clearAllStreamingFlags() {
156
- const stmt = prep('UPDATE conversations SET isStreaming = 0 WHERE isStreaming = 1');
157
- return stmt.run().changes;
158
- },
159
-
160
- markSessionIncomplete(sessionId, errorMsg) {
161
- const stmt = prep('UPDATE sessions SET status = ?, error = ?, completed_at = ? WHERE id = ?');
162
- stmt.run('incomplete', errorMsg || 'unknown', Date.now(), sessionId);
163
- },
164
-
165
- getSessionsProcessingLongerThan(minutes) {
166
- const cutoff = Date.now() - (minutes * 60 * 1000);
167
- const stmt = prep("SELECT * FROM sessions WHERE status IN ('active', 'pending') AND started_at < ?");
168
- return stmt.all(cutoff);
169
- },
170
-
171
- cleanupOrphanedSessions(days) {
172
- const cutoff = Date.now() - (days * 24 * 60 * 60 * 1000);
173
- const stmt = prep("DELETE FROM sessions WHERE status IN ('active', 'pending') AND started_at < ?");
174
- const result = stmt.run(cutoff);
175
- return result.changes || 0;
176
- },
177
-
178
- createMessage(conversationId, role, content, idempotencyKey = null) {
179
- if (idempotencyKey) {
180
- const cached = this.getIdempotencyKey(idempotencyKey);
181
- if (cached) return JSON.parse(cached);
182
- }
183
-
184
- const id = generateId('msg');
185
- const now = Date.now();
186
- const storedContent = typeof content === 'string' ? content : JSON.stringify(content);
187
-
188
- const stmt = prep(
189
- `INSERT INTO messages (id, conversationId, role, content, created_at) VALUES (?, ?, ?, ?, ?)`
190
- );
191
- stmt.run(id, conversationId, role, storedContent, now);
192
-
193
- try { prep('INSERT INTO messages_fts(rowid, content, conversationId, role) VALUES ((SELECT rowid FROM messages WHERE id = ?), ?, ?, ?)').run(id, storedContent, conversationId, role); } catch (_) {}
194
-
195
- const updateConvStmt = prep('UPDATE conversations SET updated_at = ? WHERE id = ?');
196
- updateConvStmt.run(now, conversationId);
197
-
198
- const message = {
199
- id,
200
- conversationId,
201
- role,
202
- content,
203
- created_at: now
204
- };
205
-
206
- if (idempotencyKey) {
207
- this.setIdempotencyKey(idempotencyKey, message);
208
- }
209
-
210
- return message;
211
- },
212
-
213
- getMessage(id) {
214
- const stmt = prep('SELECT * FROM messages WHERE id = ?');
215
- const msg = stmt.get(id);
216
- if (msg && typeof msg.content === 'string') {
217
- try {
218
- msg.content = JSON.parse(msg.content);
219
- } catch (_) {
220
- // If it's not JSON, leave it as string
221
- }
222
- }
223
- return msg;
224
- },
225
-
226
- getConversationMessages(conversationId) {
227
- const stmt = prep(
228
- 'SELECT * FROM messages WHERE conversationId = ? ORDER BY created_at ASC'
229
- );
230
- const messages = stmt.all(conversationId);
231
- return messages.map(msg => {
232
- if (typeof msg.content === 'string') {
233
- try {
234
- msg.content = JSON.parse(msg.content);
235
- } catch (_) {
236
- // If it's not JSON, leave it as string
237
- }
238
- }
239
- return msg;
240
- });
241
- },
242
-
243
- getLastUserMessage(conversationId) {
244
- const stmt = prep(
245
- "SELECT * FROM messages WHERE conversationId = ? AND role = 'user' ORDER BY created_at DESC LIMIT 1"
246
- );
247
- const msg = stmt.get(conversationId);
248
- if (msg && typeof msg.content === 'string') {
249
- try { msg.content = JSON.parse(msg.content); } catch (_) {}
250
- }
251
- return msg || null;
252
- },
253
-
254
- getPaginatedMessages(conversationId, limit = 50, offset = 0) {
255
- const countStmt = prep('SELECT COUNT(*) as count FROM messages WHERE conversationId = ?');
256
- const total = countStmt.get(conversationId).count;
257
-
258
- const stmt = prep(
259
- 'SELECT * FROM messages WHERE conversationId = ? ORDER BY created_at ASC LIMIT ? OFFSET ?'
260
- );
261
- const messages = stmt.all(conversationId, limit, offset);
262
-
263
- return {
264
- messages: messages.map(msg => {
265
- if (typeof msg.content === 'string') {
266
- try {
267
- msg.content = JSON.parse(msg.content);
268
- } catch (_) {
269
- // If it's not JSON, leave it as string
270
- }
271
- }
272
- return msg;
273
- }),
274
- total,
275
- limit,
276
- offset,
277
- hasMore: offset + limit < total
278
- };
279
- },
280
-
281
- createSession(conversationId) {
282
- const id = generateId('sess');
283
- const now = Date.now();
284
-
285
- const stmt = prep(
286
- `INSERT INTO sessions (id, conversationId, status, started_at, completed_at, response, error) VALUES (?, ?, ?, ?, ?, ?, ?)`
287
- );
288
- stmt.run(id, conversationId, 'pending', now, null, null, null);
289
-
290
- return {
291
- id,
292
- conversationId,
293
- status: 'pending',
294
- started_at: now,
295
- completed_at: null,
296
- response: null,
297
- error: null
298
- };
299
- },
300
-
301
- getSession(id) {
302
- const stmt = prep('SELECT * FROM sessions WHERE id = ?');
303
- return stmt.get(id);
304
- },
305
-
306
- getConversationSessions(conversationId) {
307
- const stmt = prep(
308
- 'SELECT * FROM sessions WHERE conversationId = ? ORDER BY started_at DESC'
309
- );
310
- return stmt.all(conversationId);
311
- },
312
-
313
- updateSession(id, data) {
314
- const session = this.getSession(id);
315
- if (!session) return null;
316
-
317
- const status = data.status !== undefined ? data.status : session.status;
318
- const rawResponse = data.response !== undefined ? data.response : session.response;
319
- const response = rawResponse && typeof rawResponse === 'object' ? JSON.stringify(rawResponse) : rawResponse;
320
- const error = data.error !== undefined ? data.error : session.error;
321
- const completed_at = data.completed_at !== undefined ? data.completed_at : session.completed_at;
322
-
323
- const stmt = prep(
324
- `UPDATE sessions SET status = ?, response = ?, error = ?, completed_at = ? WHERE id = ?`
325
- );
326
-
327
- try {
328
- stmt.run(status, response, error, completed_at, id);
329
- return {
330
- ...session,
331
- status,
332
- response,
333
- error,
334
- completed_at
335
- };
336
- } catch (e) {
337
- throw e;
338
- }
339
- },
340
-
341
- getLatestSession(conversationId) {
342
- const stmt = prep(
343
- 'SELECT * FROM sessions WHERE conversationId = ? ORDER BY started_at DESC LIMIT 1'
344
- );
345
- return stmt.get(conversationId) || null;
346
- },
347
-
348
- getSessionsByStatus(conversationId, status) {
349
- const stmt = prep(
350
- 'SELECT * FROM sessions WHERE conversationId = ? AND status = ? ORDER BY started_at DESC'
351
- );
352
- return stmt.all(conversationId, status);
353
- },
354
-
355
- getActiveSessions() {
356
- const stmt = prep(
357
- "SELECT * FROM sessions WHERE status IN ('active', 'pending') ORDER BY started_at DESC"
358
- );
359
- return stmt.all();
360
- },
361
-
362
- getActiveSessionConversationIds() {
363
- const stmt = prep(
364
- "SELECT DISTINCT conversationId FROM sessions WHERE status IN ('active', 'pending')"
365
- );
366
- return stmt.all().map(r => r.conversationId);
367
- },
368
-
369
- getSessionsByConversation(conversationId, limit = 10, offset = 0) {
370
- const stmt = prep(
371
- 'SELECT * FROM sessions WHERE conversationId = ? ORDER BY started_at DESC LIMIT ? OFFSET ?'
372
- );
373
- return stmt.all(conversationId, limit, offset);
374
- },
375
-
376
- getAllSessions(limit = 100) {
377
- const stmt = prep(
378
- 'SELECT * FROM sessions ORDER BY started_at DESC LIMIT ?'
379
- );
380
- return stmt.all(limit);
381
- },
382
-
383
- deleteSession(id) {
384
- const stmt = prep('DELETE FROM sessions WHERE id = ?');
385
- const result = stmt.run(id);
386
- prep('DELETE FROM chunks WHERE sessionId = ?').run(id);
387
- prep('DELETE FROM events WHERE sessionId = ?').run(id);
388
- return result.changes || 0;
389
- },
390
-
391
- createEvent(type, data, conversationId = null, sessionId = null, idempotencyKey = null) {
392
- if (idempotencyKey) {
393
- const cached = this.getIdempotencyKey(idempotencyKey);
394
- if (cached) {
395
- console.log(`[event-idempotency] Event already exists for key ${idempotencyKey}, returning cached`);
396
- return JSON.parse(cached);
397
- }
398
- }
399
-
400
- const id = generateId('evt');
401
- const now = Date.now();
402
-
403
- const stmt = prep(
404
- `INSERT INTO events (id, type, conversationId, sessionId, data, created_at) VALUES (?, ?, ?, ?, ?, ?)`
405
- );
406
- stmt.run(id, type, conversationId, sessionId, JSON.stringify(data), now);
407
-
408
- const event = {
409
- id,
410
- type,
411
- conversationId,
412
- sessionId,
413
- data,
414
- created_at: now
415
- };
416
-
417
- if (idempotencyKey) {
418
- this.setIdempotencyKey(idempotencyKey, event);
419
- }
420
-
421
- return event;
422
- },
423
-
424
- getEvent(id) {
425
- const stmt = prep('SELECT * FROM events WHERE id = ?');
426
- const row = stmt.get(id);
427
- if (row) {
428
- return {
429
- ...row,
430
- data: JSON.parse(row.data)
431
- };
432
- }
433
- return undefined;
434
- },
435
-
436
- getConversationEvents(conversationId) {
437
- const stmt = prep(
438
- 'SELECT * FROM events WHERE conversationId = ? ORDER BY created_at ASC'
439
- );
440
- const rows = stmt.all(conversationId);
441
- return rows.map(row => ({
442
- ...row,
443
- data: JSON.parse(row.data)
444
- }));
445
- },
446
-
447
- getSessionEvents(sessionId) {
448
- const stmt = prep(
449
- 'SELECT * FROM events WHERE sessionId = ? ORDER BY created_at ASC'
450
- );
451
- const rows = stmt.all(sessionId);
452
- return rows.map(row => ({
453
- ...row,
454
- data: JSON.parse(row.data)
455
- }));
456
- },
457
-
458
- deleteConversation(id) {
459
- const conv = this.getConversation(id);
460
- if (!conv) return false;
461
-
462
- // Delete all Claude Code session files for this conversation (all executions)
463
- const sessionClaudeIds = prep('SELECT DISTINCT claudeSessionId FROM sessions WHERE conversationId = ? AND claudeSessionId IS NOT NULL').all(id).map(r => r.claudeSessionId);
464
- // Also include the current claudeSessionId on the conversation record
465
- if (conv.claudeSessionId && !sessionClaudeIds.includes(conv.claudeSessionId)) {
466
- sessionClaudeIds.push(conv.claudeSessionId);
467
- }
468
- for (const csid of sessionClaudeIds) {
469
- this.deleteClaudeSessionFile(csid);
470
- }
471
-
472
- const deleteStmt = db.transaction(() => {
473
- const sessionIds = prep('SELECT id FROM sessions WHERE conversationId = ?').all(id).map(r => r.id);
474
- prep('DELETE FROM stream_updates WHERE conversationId = ?').run(id);
475
- prep('DELETE FROM chunks WHERE conversationId = ?').run(id);
476
- prep('DELETE FROM events WHERE conversationId = ?').run(id);
477
- if (sessionIds.length > 0) {
478
- const placeholders = sessionIds.map(() => '?').join(',');
479
- db.prepare(`DELETE FROM stream_updates WHERE sessionId IN (${placeholders})`).run(...sessionIds);
480
- db.prepare(`DELETE FROM chunks WHERE sessionId IN (${placeholders})`).run(...sessionIds);
481
- db.prepare(`DELETE FROM events WHERE sessionId IN (${placeholders})`).run(...sessionIds);
482
- }
483
- prep('DELETE FROM sessions WHERE conversationId = ?').run(id);
484
- prep('DELETE FROM messages WHERE conversationId = ?').run(id);
485
- prep('UPDATE conversations SET status = ?, updated_at = ? WHERE id = ?').run('deleted', Date.now(), id);
486
- });
487
-
488
- deleteStmt();
489
- return true;
490
- },
491
-
492
- deleteClaudeSessionFile(sessionId) {
493
- try {
494
- const claudeDir = path.join(os.homedir(), '.claude');
495
- const projectsDir = path.join(claudeDir, 'projects');
496
-
497
- if (!fs.existsSync(projectsDir)) {
498
- return false;
499
- }
500
-
501
- // Search for session file in all project directories
502
- const projects = fs.readdirSync(projectsDir);
503
- for (const project of projects) {
504
- const projectPath = path.join(projectsDir, project);
505
- const sessionFile = path.join(projectPath, `${sessionId}.jsonl`);
506
-
507
- if (fs.existsSync(sessionFile)) {
508
- fs.unlinkSync(sessionFile);
509
- console.log(`[deleteClaudeSessionFile] Deleted Claude session file: ${sessionFile}`);
510
-
511
- // Also remove the entry from sessions-index.json if it exists
512
- const indexPath = path.join(projectPath, 'sessions-index.json');
513
- if (fs.existsSync(indexPath)) {
514
- try {
515
- const indexContent = fs.readFileSync(indexPath, 'utf8');
516
- const index = JSON.parse(indexContent);
517
- if (index.entries && Array.isArray(index.entries)) {
518
- const originalLength = index.entries.length;
519
- index.entries = index.entries.filter(entry => entry.sessionId !== sessionId);
520
- if (index.entries.length < originalLength) {
521
- fs.writeFileSync(indexPath, JSON.stringify(index, null, 2), { encoding: 'utf8' });
522
- console.log(`[deleteClaudeSessionFile] Removed session ${sessionId} from sessions-index.json in ${projectPath}`);
523
- }
524
- }
525
- } catch (indexErr) {
526
- console.error(`[deleteClaudeSessionFile] Failed to update sessions-index.json in ${projectPath}:`, indexErr.message);
527
- }
528
- }
529
-
530
- // Also delete the session subdirectory (contains subagents/, tool-results/)
531
- const sessionDir = path.join(projectPath, sessionId);
532
- if (fs.existsSync(sessionDir)) {
533
- try {
534
- fs.rmSync(sessionDir, { recursive: true, force: true });
535
- console.log(`[deleteClaudeSessionFile] Deleted Claude session dir: ${sessionDir}`);
536
- } catch (dirErr) {
537
- console.error(`[deleteClaudeSessionFile] Failed to delete session dir ${sessionDir}:`, dirErr.message);
538
- }
539
- }
540
-
541
- return true;
542
- }
543
- }
544
-
545
- return false;
546
- } catch (err) {
547
- console.error(`[deleteClaudeSessionFile] Error deleting session ${sessionId}:`, err.message);
548
- return false;
549
- }
550
- },
551
-
552
- deleteAllConversations() {
553
- try {
554
- // Delete all Claude session files tracked per-session and per-conversation
555
- const allClaudeSessionIds = prep('SELECT DISTINCT claudeSessionId FROM sessions WHERE claudeSessionId IS NOT NULL').all().map(r => r.claudeSessionId);
556
- const convClaudeIds = prep('SELECT DISTINCT claudeSessionId FROM conversations WHERE claudeSessionId IS NOT NULL').all().map(r => r.claudeSessionId);
557
- for (const csid of convClaudeIds) {
558
- if (!allClaudeSessionIds.includes(csid)) allClaudeSessionIds.push(csid);
559
- }
560
- for (const csid of allClaudeSessionIds) {
561
- this.deleteClaudeSessionFile(csid);
562
- }
563
-
564
- const deleteAllStmt = db.transaction(() => {
565
- prep('DELETE FROM stream_updates').run();
566
- prep('DELETE FROM chunks').run();
567
- prep('DELETE FROM events').run();
568
- prep('DELETE FROM voice_cache').run();
569
- prep('DELETE FROM sessions').run();
570
- prep('DELETE FROM messages').run();
571
- prep('DELETE FROM conversations').run();
572
- });
573
-
574
- deleteAllStmt();
575
- const projectsDir = path.join(os.homedir(), '.claude', 'projects');
576
- if (fs.existsSync(projectsDir)) {
577
- for (const project of fs.readdirSync(projectsDir)) {
578
- const pdir = path.join(projectsDir, project);
579
- try {
580
- if (!fs.statSync(pdir).isDirectory()) continue;
581
- for (const entry of fs.readdirSync(pdir, { withFileTypes: true })) {
582
- if (entry.isFile() && entry.name.endsWith('.jsonl')) {
583
- fs.unlinkSync(path.join(pdir, entry.name));
584
- } else if (entry.isDirectory()) {
585
- fs.rmSync(path.join(pdir, entry.name), { recursive: true, force: true });
586
- }
587
- }
588
- } catch (err) {
589
- console.error('[deleteAllConversations] Failed to clean project dir:', pdir, err.message);
590
- }
591
- }
592
- }
593
- console.log('[deleteAllConversations] Deleted all conversations and associated Claude Code files');
594
- return true;
595
- } catch (err) {
596
- console.error('[deleteAllConversations] Error deleting all conversations:', err.message);
597
- return false;
598
- }
599
- },
600
-
601
- cleanup() {
602
- const thirtyDaysAgo = Date.now() - (30 * 24 * 60 * 60 * 1000);
603
- const sevenDaysAgo = Date.now() - (7 * 24 * 60 * 60 * 1000);
604
- const now = Date.now();
605
-
606
- const cleanupStmt = db.transaction(() => {
607
- // Core tables - time-based retention
608
- prep('DELETE FROM events WHERE created_at < ?').run(thirtyDaysAgo);
609
- prep('DELETE FROM sessions WHERE completed_at IS NOT NULL AND completed_at < ?').run(thirtyDaysAgo);
610
- prep('DELETE FROM idempotencyKeys WHERE (created_at + ttl) < ?').run(now);
611
-
612
- // Chunks and stream_updates: completed sessions >7 days
613
- prep('DELETE FROM chunks WHERE sessionId IN (SELECT id FROM sessions WHERE completed_at IS NOT NULL AND completed_at < ?)').run(sevenDaysAgo);
614
- prep('DELETE FROM stream_updates WHERE sessionId IN (SELECT id FROM sessions WHERE completed_at IS NOT NULL AND completed_at < ?)').run(sevenDaysAgo);
615
-
616
- // Chunks and stream_updates: orphaned (session missing or stuck non-completed >7 days)
617
- prep('DELETE FROM chunks WHERE created_at < ? AND sessionId NOT IN (SELECT id FROM sessions WHERE completed_at IS NULL AND started_at >= ?)').run(sevenDaysAgo, sevenDaysAgo);
618
- prep('DELETE FROM stream_updates WHERE created_at < ? AND sessionId NOT IN (SELECT id FROM sessions WHERE completed_at IS NULL AND started_at >= ?)').run(sevenDaysAgo, sevenDaysAgo);
619
-
620
- // Clean expired voice cache
621
- prep('DELETE FROM voice_cache WHERE expires_at <= ?').run(now);
622
-
623
- // Hard-delete soft-deleted conversations and their orphaned data after 7 days
624
- const deletedConvIds = prep("SELECT id FROM conversations WHERE status = 'deleted' AND updated_at < ?").all(sevenDaysAgo).map(r => r.id);
625
- for (const cid of deletedConvIds) {
626
- prep('DELETE FROM stream_updates WHERE conversationId = ?').run(cid);
627
- prep('DELETE FROM chunks WHERE conversationId = ?').run(cid);
628
- prep('DELETE FROM events WHERE conversationId = ?').run(cid);
629
- prep('DELETE FROM sessions WHERE conversationId = ?').run(cid);
630
- prep('DELETE FROM messages WHERE conversationId = ?').run(cid);
631
- prep('DELETE FROM voice_cache WHERE conversationId = ?').run(cid);
632
- prep('DELETE FROM thread_states WHERE thread_id = ?').run(cid);
633
- prep('DELETE FROM checkpoints WHERE thread_id = ?').run(cid);
634
- prep('DELETE FROM run_metadata WHERE thread_id = ?').run(cid);
635
- prep('DELETE FROM conversations WHERE id = ?').run(cid);
636
- }
637
-
638
- // ACP tables: prune old thread_states, checkpoints, run_metadata (30 days)
639
- prep('DELETE FROM thread_states WHERE created_at < ?').run(thirtyDaysAgo);
640
- prep('DELETE FROM checkpoints WHERE created_at < ?').run(thirtyDaysAgo);
641
- prep('DELETE FROM run_metadata WHERE created_at < ? AND status NOT IN (?, ?)').run(thirtyDaysAgo, 'active', 'pending');
642
-
643
- // Workflow runs older than 30 days
644
- prep('DELETE FROM workflow_runs WHERE created_at < ?').run(thirtyDaysAgo);
645
-
646
- // Tool install history: keep last 50 per tool
647
- const toolIds = prep('SELECT DISTINCT tool_id FROM tool_install_history').all().map(r => r.tool_id);
648
- for (const tid of toolIds) {
649
- prep(`DELETE FROM tool_install_history WHERE tool_id = ? AND id NOT IN (
650
- SELECT id FROM tool_install_history WHERE tool_id = ? ORDER BY created_at DESC LIMIT 50
651
- )`).run(tid, tid);
652
- }
653
- });
654
-
655
- cleanupStmt();
656
-
657
- // Incremental VACUUM to reclaim space without blocking (WAL-safe)
658
- try {
659
- db.exec('PRAGMA incremental_vacuum(1000)');
660
- } catch (_) {
661
- // incremental_vacuum requires auto_vacuum=INCREMENTAL, fall through
662
- }
663
- },
664
-
665
- setIdempotencyKey(key, value) {
666
- const now = Date.now();
667
- const ttl = 24 * 60 * 60 * 1000;
668
-
669
- const stmt = prep(
670
- 'INSERT OR REPLACE INTO idempotencyKeys (key, value, created_at, ttl) VALUES (?, ?, ?, ?)'
671
- );
672
- stmt.run(key, JSON.stringify(value), now, ttl);
673
- },
674
-
675
- getIdempotencyKey(key) {
676
- const stmt = prep('SELECT * FROM idempotencyKeys WHERE key = ?');
677
- const entry = stmt.get(key);
678
-
679
- if (!entry) return null;
680
-
681
- const isExpired = Date.now() - entry.created_at > entry.ttl;
682
- if (isExpired) {
683
- db.run('DELETE FROM idempotencyKeys WHERE key = ?', [key]);
684
- return null;
685
- }
686
-
687
- return entry.value;
688
- },
689
-
690
- clearIdempotencyKey(key) {
691
- db.run('DELETE FROM idempotencyKeys WHERE key = ?', [key]);
692
- },
693
-
694
- discoverClaudeCodeConversations() {
695
- const projectsDir = path.join(os.homedir(), '.claude', 'projects');
696
- if (!fs.existsSync(projectsDir)) return [];
697
-
698
- const discovered = [];
699
- try {
700
- const dirs = fs.readdirSync(projectsDir, { withFileTypes: true });
701
- for (const dir of dirs) {
702
- if (!dir.isDirectory()) continue;
703
- const dirPath = path.join(projectsDir, dir.name);
704
- const indexPath = path.join(dirPath, 'sessions-index.json');
705
- if (!fs.existsSync(indexPath)) continue;
706
-
707
- try {
708
- const index = JSON.parse(fs.readFileSync(indexPath, 'utf-8'));
709
- const projectPath = index.originalPath || dir.name.replace(/^-/, '/').replace(/-/g, '/');
710
- for (const entry of (index.entries || [])) {
711
- if (!entry.sessionId || entry.messageCount === 0) continue;
712
- discovered.push({
713
- id: entry.sessionId,
714
- jsonlPath: entry.fullPath || path.join(dirPath, `${entry.sessionId}.jsonl`),
715
- title: entry.summary || entry.firstPrompt || 'Claude Code Session',
716
- projectPath,
717
- created: entry.created ? new Date(entry.created).getTime() : entry.fileMtime,
718
- modified: entry.modified ? new Date(entry.modified).getTime() : entry.fileMtime,
719
- messageCount: entry.messageCount,
720
- gitBranch: entry.gitBranch,
721
- source: 'claude-code'
722
- });
723
- }
724
- } catch (e) {
725
- console.error(`Error reading index ${indexPath}:`, e.message);
726
- }
727
- }
728
- } catch (e) {
729
- console.error('Error discovering Claude Code conversations:', e.message);
730
- }
731
-
732
- return discovered;
733
- },
734
-
735
- parseJsonlMessages(jsonlPath) {
736
- if (!fs.existsSync(jsonlPath)) return [];
737
- const messages = [];
738
- try {
739
- const lines = fs.readFileSync(jsonlPath, 'utf-8').split('\n');
740
- for (const line of lines) {
741
- if (!line.trim()) continue;
742
- try {
743
- const obj = JSON.parse(line);
744
- if (obj.type === 'user' && obj.message?.content) {
745
- const content = typeof obj.message.content === 'string'
746
- ? obj.message.content
747
- : Array.isArray(obj.message.content)
748
- ? obj.message.content.filter(c => c.type === 'text').map(c => c.text).join('\n')
749
- : JSON.stringify(obj.message.content);
750
- if (content && !content.startsWith('[{"tool_use_id"')) {
751
- messages.push({ id: obj.uuid || generateId('msg'), role: 'user', content, created_at: new Date(obj.timestamp).getTime() });
752
- }
753
- } else if (obj.type === 'assistant' && obj.message?.content) {
754
- let text = '';
755
- const content = obj.message.content;
756
- if (Array.isArray(content)) {
757
- // CRITICAL FIX: Join text blocks with newlines to preserve separation
758
- const textBlocks = [];
759
- for (const c of content) {
760
- if (c.type === 'text' && c.text) {
761
- textBlocks.push(c.text);
762
- }
763
- }
764
- // Join with double newline to preserve logical separation
765
- text = textBlocks.join('\n\n');
766
- } else if (typeof content === 'string') {
767
- text = content;
768
- }
769
- if (text) {
770
- messages.push({ id: obj.uuid || generateId('msg'), role: 'assistant', content: text, created_at: new Date(obj.timestamp).getTime() });
771
- }
772
- }
773
- } catch (_) {}
774
- }
775
- } catch (e) {
776
- console.error(`Error parsing JSONL ${jsonlPath}:`, e.message);
777
- }
778
- return messages;
779
- },
780
-
781
- importClaudeCodeConversations() {
782
- const discovered = this.discoverClaudeCodeConversations();
783
- const imported = [];
784
-
785
- for (const conv of discovered) {
786
- try {
787
- const existingConv = prep('SELECT id, status FROM conversations WHERE id = ? OR externalId = ?').get(conv.id, conv.id);
788
- if (existingConv) {
789
- imported.push({ id: conv.id, status: 'skipped', reason: existingConv.status === 'deleted' ? 'deleted' : 'exists' });
790
- continue;
791
- }
792
-
793
- const projectName = conv.projectPath ? path.basename(conv.projectPath) : '';
794
- const title = conv.title || 'Claude Code Session';
795
- const displayTitle = projectName ? `[${projectName}] ${title}` : title;
796
-
797
- const messages = this.parseJsonlMessages(conv.jsonlPath);
798
-
799
- const importStmt = db.transaction(() => {
800
- prep(
801
- `INSERT INTO conversations (id, agentId, title, created_at, updated_at, status, claudeSessionId) VALUES (?, ?, ?, ?, ?, ?, ?)`
802
- ).run(conv.id, 'claude-code', displayTitle, conv.created, conv.modified, 'active', conv.id);
803
-
804
- for (const msg of messages) {
805
- try {
806
- prep(
807
- `INSERT INTO messages (id, conversationId, role, content, created_at) VALUES (?, ?, ?, ?, ?)`
808
- ).run(msg.id, conv.id, msg.role, msg.content, msg.created_at);
809
- } catch (_) {}
810
- }
811
- });
812
-
813
- importStmt();
814
- imported.push({ id: conv.id, status: 'imported', title: displayTitle, messages: messages.length });
815
- } catch (e) {
816
- imported.push({ id: conv.id, status: 'error', error: e.message });
817
- }
818
- }
819
-
820
- return imported;
821
- },
822
-
823
- createStreamUpdate(sessionId, conversationId, updateType, content) {
824
- const id = generateId('upd');
825
- const now = Date.now();
826
-
827
- // Use transaction to ensure atomic sequence number assignment
828
- const transaction = db.transaction(() => {
829
- const maxSequence = prep(
830
- 'SELECT MAX(sequence) as max FROM stream_updates WHERE sessionId = ?'
831
- ).get(sessionId);
832
- const sequence = (maxSequence?.max || -1) + 1;
833
-
834
- prep(
835
- `INSERT INTO stream_updates (id, sessionId, conversationId, updateType, content, sequence, created_at)
836
- VALUES (?, ?, ?, ?, ?, ?, ?)`
837
- ).run(id, sessionId, conversationId, updateType, JSON.stringify(content), sequence, now);
838
-
839
- return sequence;
840
- });
841
-
842
- const sequence = transaction();
843
-
844
- return {
845
- id,
846
- sessionId,
847
- conversationId,
848
- updateType,
849
- content,
850
- sequence,
851
- created_at: now
852
- };
853
- },
854
-
855
- getSessionStreamUpdates(sessionId) {
856
- const stmt = prep(
857
- `SELECT id, sessionId, conversationId, updateType, content, sequence, created_at
858
- FROM stream_updates WHERE sessionId = ? ORDER BY sequence ASC`
859
- );
860
- const rows = stmt.all(sessionId);
861
- return rows.map(row => ({
862
- ...row,
863
- content: JSON.parse(row.content)
864
- }));
865
- },
866
-
867
- clearSessionStreamUpdates(sessionId) {
868
- const stmt = prep('DELETE FROM stream_updates WHERE sessionId = ?');
869
- stmt.run(sessionId);
870
- },
871
-
872
- createImportedConversation(data) {
873
- const id = generateId('conv');
874
- const now = Date.now();
875
- const stmt = prep(
876
- `INSERT INTO conversations (
877
- id, agentId, title, created_at, updated_at, status,
878
- agentType, source, externalId, firstPrompt, messageCount,
879
- projectPath, gitBranch, sourcePath, lastSyncedAt
880
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
881
- );
882
- stmt.run(
883
- id,
884
- data.externalId || id,
885
- data.title,
886
- data.created || now,
887
- data.modified || now,
888
- 'active',
889
- data.agentType || 'claude-code',
890
- data.source || 'imported',
891
- data.externalId,
892
- data.firstPrompt,
893
- data.messageCount || 0,
894
- data.projectPath,
895
- data.gitBranch,
896
- data.sourcePath,
897
- now
898
- );
899
- return { id, ...data };
900
- },
901
-
902
- getConversationByExternalId(agentType, externalId) {
903
- const stmt = prep(
904
- 'SELECT * FROM conversations WHERE agentType = ? AND externalId = ?'
905
- );
906
- return stmt.get(agentType, externalId);
907
- },
908
-
909
- getConversationsByAgentType(agentType) {
910
- const stmt = prep(
911
- 'SELECT * FROM conversations WHERE agentType = ? AND status != ? ORDER BY updated_at DESC'
912
- );
913
- return stmt.all(agentType, 'deleted');
914
- },
915
-
916
- getImportedConversations() {
917
- const stmt = prep(
918
- 'SELECT * FROM conversations WHERE source = ? AND status != ? ORDER BY updated_at DESC'
919
- );
920
- return stmt.all('imported', 'deleted');
921
- },
922
-
923
- createChunk(sessionId, conversationId, sequence, type, data) {
924
- const id = generateId('chunk');
925
- const now = Date.now();
926
- const dataBlob = typeof data === 'string' ? data : JSON.stringify(data);
927
-
928
- const stmt = prep(
929
- `INSERT INTO chunks (id, sessionId, conversationId, sequence, type, data, created_at)
930
- VALUES (?, ?, ?, ?, ?, ?, ?)`
931
- );
932
- stmt.run(id, sessionId, conversationId, sequence, type, dataBlob, now);
933
-
934
- return {
935
- id,
936
- sessionId,
937
- conversationId,
938
- sequence,
939
- type,
940
- data,
941
- created_at: now
942
- };
943
- },
944
-
945
- getChunk(id) {
946
- const stmt = prep(
947
- `SELECT id, sessionId, conversationId, sequence, type, data, created_at FROM chunks WHERE id = ?`
948
- );
949
- const row = stmt.get(id);
950
- if (!row) return null;
951
-
952
- try {
953
- return {
954
- ...row,
955
- data: typeof row.data === 'string' ? JSON.parse(row.data) : row.data
956
- };
957
- } catch (e) {
958
- return row;
959
- }
960
- },
961
-
962
- getSessionChunks(sessionId) {
963
- const stmt = prep(
964
- `SELECT id, sessionId, conversationId, sequence, type, data, created_at
965
- FROM chunks WHERE sessionId = ? ORDER BY sequence ASC`
966
- );
967
- const rows = stmt.all(sessionId);
968
- return rows.map(row => {
969
- try {
970
- return {
971
- ...row,
972
- data: typeof row.data === 'string' ? JSON.parse(row.data) : row.data
973
- };
974
- } catch (e) {
975
- return row;
976
- }
977
- });
978
- },
979
-
980
- getConversationChunkCount(conversationId) {
981
- const stmt = prep('SELECT COUNT(*) as count FROM chunks WHERE conversationId = ?');
982
- return stmt.get(conversationId).count;
983
- },
984
-
985
- getConversationChunks(conversationId) {
986
- const stmt = prep(
987
- `SELECT id, sessionId, conversationId, sequence, type, data, created_at
988
- FROM chunks WHERE conversationId = ? ORDER BY created_at ASC`
989
- );
990
- const rows = stmt.all(conversationId);
991
- return rows.map(row => {
992
- try {
993
- return {
994
- ...row,
995
- data: typeof row.data === 'string' ? JSON.parse(row.data) : row.data
996
- };
997
- } catch (e) {
998
- return row;
999
- }
1000
- });
1001
- },
1002
-
1003
- getConversationChunksSince(conversationId, since) {
1004
- const stmt = prep(
1005
- `SELECT id, sessionId, conversationId, sequence, type, data, created_at
1006
- FROM chunks WHERE conversationId = ? AND created_at > ? ORDER BY created_at ASC`
1007
- );
1008
- const rows = stmt.all(conversationId, since);
1009
- return rows.map(row => {
1010
- try { return { ...row, data: typeof row.data === 'string' ? JSON.parse(row.data) : row.data }; }
1011
- catch (e) { return row; }
1012
- });
1013
- },
1014
-
1015
- getRecentConversationChunks(conversationId, limit = 500) {
1016
- const sessions = prep(
1017
- `SELECT sessionId, COUNT(*) as n FROM chunks WHERE conversationId = ?
1018
- GROUP BY sessionId ORDER BY MAX(created_at) DESC`
1019
- ).all(conversationId);
1020
- if (!sessions.length) return [];
1021
- const included = [];
1022
- let total = 0;
1023
- for (const s of sessions) {
1024
- if (total + s.n > limit && included.length > 0) break;
1025
- included.unshift(s.sessionId);
1026
- total += s.n;
1027
- }
1028
- const placeholders = included.map(() => '?').join(',');
1029
- const rows = prep(
1030
- `SELECT id, sessionId, conversationId, sequence, type, data, created_at
1031
- FROM chunks WHERE conversationId = ? AND sessionId IN (${placeholders})
1032
- ORDER BY created_at ASC`
1033
- ).all(conversationId, ...included);
1034
- return rows.map(row => {
1035
- try { return { ...row, data: typeof row.data === 'string' ? JSON.parse(row.data) : row.data }; }
1036
- catch (e) { return row; }
1037
- });
1038
- },
1039
-
1040
- getRecentMessages(conversationId, limit = 20) {
1041
- const countStmt = prep('SELECT COUNT(*) as count FROM messages WHERE conversationId = ?');
1042
- const total = countStmt.get(conversationId).count;
1043
-
1044
- const stmt = prep(
1045
- 'SELECT * FROM messages WHERE conversationId = ? ORDER BY created_at DESC LIMIT ? '
1046
- );
1047
- const messages = stmt.all(conversationId, limit).reverse();
1048
-
1049
- return {
1050
- messages: messages.map(msg => {
1051
- if (typeof msg.content === 'string') {
1052
- try {
1053
- msg.content = JSON.parse(msg.content);
1054
- } catch (_) {}
1055
- }
1056
- return msg;
1057
- }),
1058
- total,
1059
- limit,
1060
- offset: Math.max(0, total - limit),
1061
- hasMore: total > limit
1062
- };
1063
- },
1064
-
1065
- getMessagesBefore(conversationId, beforeId, limit = 50) {
1066
- const countStmt = prep('SELECT COUNT(*) as count FROM messages WHERE conversationId = ?');
1067
- const total = countStmt.get(conversationId).count;
1068
-
1069
- const stmt = prep(`
1070
- SELECT * FROM messages
1071
- WHERE conversationId = ? AND id < (SELECT id FROM messages WHERE id = ?)
1072
- ORDER BY created_at DESC LIMIT ?
1073
- `);
1074
- const messages = stmt.all(conversationId, beforeId, limit).reverse();
1075
-
1076
- return {
1077
- messages: messages.map(msg => {
1078
- if (typeof msg.content === 'string') {
1079
- try {
1080
- msg.content = JSON.parse(msg.content);
1081
- } catch (_) {}
1082
- }
1083
- return msg;
1084
- }),
1085
- total,
1086
- limit,
1087
- hasMore: total > (limit + 1)
1088
- };
1089
- },
1090
-
1091
- getChunksBefore(conversationId, beforeTimestamp, limit = 500) {
1092
- const total = prep('SELECT COUNT(*) as count FROM chunks WHERE conversationId = ? AND created_at < ?')
1093
- .get(conversationId, beforeTimestamp).count;
1094
-
1095
- const rows = prep(`
1096
- SELECT id, sessionId, conversationId, sequence, type, data, created_at
1097
- FROM chunks
1098
- WHERE conversationId = ? AND created_at < ?
1099
- ORDER BY created_at DESC LIMIT ?
1100
- `).all(conversationId, beforeTimestamp, limit);
1101
- rows.reverse();
1102
-
1103
- return {
1104
- chunks: rows.map(row => {
1105
- try {
1106
- return { ...row, data: typeof row.data === 'string' ? JSON.parse(row.data) : row.data };
1107
- } catch (e) {
1108
- return row;
1109
- }
1110
- }),
1111
- total,
1112
- limit,
1113
- hasMore: rows.length === limit
1114
- };
1115
- },
1116
-
1117
- getChunksSince(sessionId, timestamp) {
1118
- const stmt = prep(
1119
- `SELECT id, sessionId, conversationId, sequence, type, data, created_at
1120
- FROM chunks WHERE sessionId = ? AND created_at > ? ORDER BY sequence ASC`
1121
- );
1122
- const rows = stmt.all(sessionId, timestamp);
1123
- return rows.map(row => {
1124
- try {
1125
- return {
1126
- ...row,
1127
- data: typeof row.data === 'string' ? JSON.parse(row.data) : row.data
1128
- };
1129
- } catch (e) {
1130
- return row;
1131
- }
1132
- });
1133
- },
1134
-
1135
- getChunksSinceSeq(sessionId, sinceSeq) {
1136
- const stmt = prep(
1137
- `SELECT id, sessionId, conversationId, sequence, type, data, created_at
1138
- FROM chunks WHERE sessionId = ? AND sequence > ? ORDER BY sequence ASC`
1139
- );
1140
- const rows = stmt.all(sessionId, sinceSeq);
1141
- return rows.map(row => {
1142
- try {
1143
- return {
1144
- ...row,
1145
- data: typeof row.data === 'string' ? JSON.parse(row.data) : row.data
1146
- };
1147
- } catch (e) {
1148
- return row;
1149
- }
1150
- });
1151
- },
1152
-
1153
- deleteSessionChunks(sessionId) {
1154
- const stmt = prep('DELETE FROM chunks WHERE sessionId = ?');
1155
- const result = stmt.run(sessionId);
1156
- return result.changes || 0;
1157
- },
1158
-
1159
- getMaxSequence(sessionId) {
1160
- const stmt = prep('SELECT MAX(sequence) as max FROM chunks WHERE sessionId = ?');
1161
- const result = stmt.get(sessionId);
1162
- return result?.max ?? -1;
1163
- },
1164
-
1165
- getEmptyConversations() {
1166
- const stmt = prep(`
1167
- SELECT c.* FROM conversations c
1168
- LEFT JOIN messages m ON c.id = m.conversationId
1169
- WHERE c.status != 'deleted'
1170
- GROUP BY c.id
1171
- HAVING COUNT(m.id) = 0
1172
- `);
1173
- return stmt.all();
1174
- },
1175
-
1176
- permanentlyDeleteConversation(id) {
1177
- return this.deleteConversation(id);
1178
- },
1179
-
1180
- cleanupEmptyConversations() {
1181
- const emptyConvs = this.getEmptyConversations();
1182
- let deletedCount = 0;
1183
-
1184
- for (const conv of emptyConvs) {
1185
- console.log(`[cleanup] Deleting empty conversation: ${conv.id} (${conv.title || 'Untitled'})`);
1186
- if (this.permanentlyDeleteConversation(conv.id)) {
1187
- deletedCount++;
1188
- }
1189
- }
1190
-
1191
- if (deletedCount > 0) {
1192
- console.log(`[cleanup] Deleted ${deletedCount} empty conversation(s)`);
1193
- }
1194
-
1195
- return deletedCount;
1196
- },
1197
-
1198
-
1199
- getDownloadsByStatus(status) {
1200
- const stmt = prep('SELECT * FROM WHERE status = ? ORDER BY started_at DESC');
1201
- return stmt.all(status);
1202
- },
1203
-
1204
- updateDownloadResume(downloadId, currentSize, attempts, lastAttempt, status) {
1205
- const stmt = prep(`
1206
- UPDATE
1207
- SET downloaded_bytes = ?, attempts = ?, lastAttempt = ?, status = ?
1208
- WHERE id = ?
1209
- `);
1210
- stmt.run(currentSize, attempts, lastAttempt, status, downloadId);
1211
- },
1212
-
1213
- updateDownloadHash(downloadId, hash) {
1214
- const stmt = prep('UPDATE SET hash = ? WHERE id = ?');
1215
- stmt.run(hash, downloadId);
1216
- },
1217
-
1218
- markDownloadResuming(downloadId) {
1219
- const stmt = prep('UPDATE SET status = ?, lastAttempt = ? WHERE id = ?');
1220
- stmt.run('resuming', Date.now(), downloadId);
1221
- },
1222
-
1223
- markDownloadPaused(downloadId, errorMessage) {
1224
- const stmt = prep('UPDATE SET status = ?, error_message = ?, lastAttempt = ? WHERE id = ?');
1225
- stmt.run('paused', errorMessage, Date.now(), downloadId);
1226
- },
1227
-
1228
- saveVoiceCache(conversationId, text, audioBlob, ttlMs = 3600000) {
1229
- const id = generateId('vcache');
1230
- const now = Date.now();
1231
- const expiresAt = now + ttlMs;
1232
- const byteSize = audioBlob ? Buffer.byteLength(audioBlob) : 0;
1233
- const stmt = prep(`
1234
- INSERT INTO voice_cache (id, conversationId, text, audioBlob, byteSize, created_at, expires_at)
1235
- VALUES (?, ?, ?, ?, ?, ?, ?)
1236
- `);
1237
- stmt.run(id, conversationId, text, audioBlob || null, byteSize, now, expiresAt);
1238
- return { id, conversationId, text, byteSize, created_at: now, expires_at: expiresAt };
1239
- },
1240
-
1241
- getVoiceCache(conversationId, text) {
1242
- const now = Date.now();
1243
- const stmt = prep(`
1244
- SELECT id, conversationId, text, audioBlob, byteSize, created_at, expires_at
1245
- FROM voice_cache
1246
- WHERE conversationId = ? AND text = ? AND expires_at > ?
1247
- LIMIT 1
1248
- `);
1249
- return stmt.get(conversationId, text, now) || null;
1250
- },
1251
-
1252
- cleanExpiredVoiceCache() {
1253
- const now = Date.now();
1254
- const stmt = prep('DELETE FROM voice_cache WHERE expires_at <= ?');
1255
- return stmt.run(now).changes;
1256
- },
1257
-
1258
- getVoiceCacheSize(conversationId) {
1259
- const now = Date.now();
1260
- const stmt = prep(`
1261
- SELECT COALESCE(SUM(byteSize), 0) as totalSize
1262
- FROM voice_cache
1263
- WHERE conversationId = ? AND expires_at > ?
1264
- `);
1265
- return stmt.get(conversationId, now).totalSize || 0;
1266
- },
1267
-
1268
- deleteOldestVoiceCache(conversationId, neededBytes) {
1269
- const stmt = prep(`
1270
- SELECT id FROM voice_cache
1271
- WHERE conversationId = ?
1272
- ORDER BY created_at ASC
1273
- LIMIT (SELECT COUNT(*) FROM voice_cache WHERE conversationId = ? AND byteSize > ?)
1274
- `);
1275
- const oldest = stmt.all(conversationId, conversationId, neededBytes);
1276
- const deleteStmt = prep('DELETE FROM voice_cache WHERE id = ?');
1277
- for (const row of oldest) {
1278
- deleteStmt.run(row.id);
1279
- }
1280
- return oldest.length;
1281
- },
1282
-
1283
- initializeToolInstallations(tools) {
1284
- const now = Date.now();
1285
- for (const tool of tools) {
1286
- const stmt = prep(`
1287
- INSERT OR IGNORE INTO tool_installations
1288
- (id, tool_id, status, created_at, updated_at)
1289
- VALUES (?, ?, ?, ?, ?)
1290
- `);
1291
- stmt.run(generateId('tinst'), tool.id, 'not_installed', now, now);
1292
- }
1293
- },
1294
-
1295
- getToolStatus(toolId) {
1296
- const stmt = prep(`
1297
- SELECT id, tool_id, version, installed_at, status, last_check_at,
1298
- error_message, update_available, latest_version, created_at, updated_at
1299
- FROM tool_installations
1300
- WHERE tool_id = ?
1301
- `);
1302
- return stmt.get(toolId) || null;
1303
- },
1304
-
1305
- getAllToolStatuses() {
1306
- const stmt = prep(`
1307
- SELECT id, tool_id, version, installed_at, status, last_check_at,
1308
- error_message, update_available, latest_version, created_at, updated_at
1309
- FROM tool_installations
1310
- ORDER BY tool_id
1311
- `);
1312
- return stmt.all();
1313
- },
1314
-
1315
- insertToolInstallation(toolId, data) {
1316
- const now = Date.now();
1317
- const stmt = prep(`
1318
- INSERT OR IGNORE INTO tool_installations
1319
- (id, tool_id, version, installed_at, status, last_check_at, error_message, update_available, latest_version, created_at, updated_at)
1320
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1321
- `);
1322
- stmt.run(
1323
- generateId('ti'),
1324
- toolId,
1325
- data.version || null,
1326
- data.installed_at || null,
1327
- data.status || 'not_installed',
1328
- now,
1329
- data.error_message || null,
1330
- 0,
1331
- null,
1332
- now,
1333
- now
1334
- );
1335
- },
1336
-
1337
- updateToolStatus(toolId, data) {
1338
- const now = Date.now();
1339
- const stmt = prep(`
1340
- UPDATE tool_installations
1341
- SET version = ?, installed_at = ?, status = ?, last_check_at = ?,
1342
- error_message = ?, update_available = ?, latest_version = ?, updated_at = ?
1343
- WHERE tool_id = ?
1344
- `);
1345
- stmt.run(
1346
- data.version || null,
1347
- data.installed_at || null,
1348
- data.status || 'not_installed',
1349
- data.last_check_at || now,
1350
- data.error_message || null,
1351
- data.update_available ? 1 : 0,
1352
- data.latest_version || null,
1353
- now,
1354
- toolId
1355
- );
1356
- },
1357
-
1358
- addToolInstallHistory(toolId, action, status, error) {
1359
- const now = Date.now();
1360
- const stmt = prep(`
1361
- INSERT INTO tool_install_history
1362
- (id, tool_id, action, started_at, completed_at, status, error_message, created_at)
1363
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)
1364
- `);
1365
- stmt.run(generateId('thist'), toolId, action, now, now, status, error || null, now);
1366
- },
1367
-
1368
- getToolInstallHistory(toolId, limit = 20, offset = 0) {
1369
- const stmt = prep(`
1370
- SELECT id, tool_id, action, started_at, completed_at, status, error_message, created_at
1371
- FROM tool_install_history
1372
- WHERE tool_id = ?
1373
- ORDER BY created_at DESC
1374
- LIMIT ? OFFSET ?
1375
- `);
1376
- return stmt.all(toolId, limit, offset);
1377
- },
1378
-
1379
- pruneToolInstallHistory(toolId, keepCount = 100) {
1380
- const stmt = prep(`
1381
- DELETE FROM tool_install_history
1382
- WHERE tool_id = ? AND id NOT IN (
1383
- SELECT id FROM tool_install_history
1384
- WHERE tool_id = ?
1385
- ORDER BY created_at DESC
1386
- LIMIT ?
1387
- )
1388
- `);
1389
- return stmt.run(toolId, toolId, keepCount).changes;
1390
- },
1391
-
1392
- searchMessages(query, limit = 50) {
1393
- try {
1394
- // Escape FTS5 special characters and wrap as phrase query
1395
- const sanitized = '"' + query.replace(/"/g, '""') + '"';
1396
- const stmt = prep(`
1397
- SELECT m.id, m.conversationId, m.role, m.content, m.created_at,
1398
- c.title as conversationTitle, c.agentType
1399
- FROM messages_fts fts
1400
- JOIN messages m ON m.rowid = fts.rowid
1401
- JOIN conversations c ON c.id = m.conversationId
1402
- WHERE messages_fts MATCH ?
1403
- ORDER BY m.created_at DESC LIMIT ?
1404
- `);
1405
- return stmt.all(sanitized, limit);
1406
- } catch (_) { return []; }
1407
- },
1408
-
1409
- // ============ ACP-COMPATIBLE QUERIES ============
1410
- ...createACPQueries(db, prep)
1411
- };
1412
- }
1
+ import { createACPQueries } from '../acp-queries.js';
2
+ import { addMessageQueries } from './db-queries-messages.js';
3
+ import { addSessionQueries } from './db-queries-sessions.js';
4
+ import { addEventQueries } from './db-queries-events.js';
5
+ import { addDeleteQueries } from './db-queries-del.js';
6
+ import { addCleanupQueries } from './db-queries-cleanup.js';
7
+ import { addImportQueries } from './db-queries-import.js';
8
+ import { addStreamQueries } from './db-queries-streams.js';
9
+ import { addChunkQueries } from './db-queries-chunks.js';
10
+ import { addChunkQueries2 } from './db-queries-chunks2.js';
11
+ import { addVoiceQueries } from './db-queries-voice.js';
12
+ import { addToolQueries } from './db-queries-tools.js';
13
+
14
+ export function createQueries(db, prep, generateId) {
15
+ const q = {
16
+ _db: db,
17
+ createConversation(agentType, title = null, workingDirectory = null, model = null, subAgent = null) {
18
+ const id = generateId('conv');
19
+ const now = Date.now();
20
+ const stmt = prep(
21
+ `INSERT INTO conversations (id, agentId, agentType, title, created_at, updated_at, status, workingDirectory, model, subAgent) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
22
+ );
23
+ stmt.run(id, agentType, agentType, title, now, now, 'active', workingDirectory, model, subAgent);
24
+ return { id, agentType, title, workingDirectory, model, subAgent, created_at: now, updated_at: now, status: 'active' };
25
+ },
26
+ getConversation(id) { return prep('SELECT * FROM conversations WHERE id = ?').get(id); },
27
+ getAllConversations() { return prep('SELECT * FROM conversations WHERE status != ? ORDER BY updated_at DESC').all('deleted'); },
28
+ getConversationsList() {
29
+ return prep('SELECT id, agentId, title, agentType, created_at, updated_at, messageCount, workingDirectory, isStreaming, model, subAgent, pinned, sortOrder, tags FROM conversations WHERE status NOT IN (?, ?) ORDER BY pinned DESC, sortOrder ASC, updated_at DESC').all('deleted', 'archived');
30
+ },
31
+ getConversations() { return prep('SELECT * FROM conversations WHERE status NOT IN (?, ?) ORDER BY pinned DESC, sortOrder ASC, updated_at DESC').all('deleted', 'archived'); },
32
+ updateConversationSortOrder(id, sortOrder) { prep('UPDATE conversations SET sortOrder = ? WHERE id = ?').run(sortOrder, id); },
33
+ getArchivedConversations() { return prep('SELECT id, agentId, title, agentType, created_at, updated_at, messageCount, workingDirectory, model, subAgent FROM conversations WHERE status = ? ORDER BY updated_at DESC').all('archived'); },
34
+ archiveConversation(id) {
35
+ const conv = this.getConversation(id);
36
+ if (!conv) return null;
37
+ prep('UPDATE conversations SET status = ?, updated_at = ? WHERE id = ?').run('archived', Date.now(), id);
38
+ return this.getConversation(id);
39
+ },
40
+ restoreConversation(id) {
41
+ const conv = this.getConversation(id);
42
+ if (!conv) return null;
43
+ prep('UPDATE conversations SET status = ?, updated_at = ? WHERE id = ?').run('active', Date.now(), id);
44
+ return this.getConversation(id);
45
+ },
46
+ updateConversation(id, data) {
47
+ const conv = this.getConversation(id);
48
+ if (!conv) return null;
49
+ const now = Date.now();
50
+ const title = data.title !== undefined ? data.title : conv.title;
51
+ const status = data.status !== undefined ? data.status : conv.status;
52
+ const agentId = data.agentId !== undefined ? data.agentId : conv.agentId;
53
+ const agentType = data.agentType !== undefined ? data.agentType : conv.agentType;
54
+ const model = data.model !== undefined ? data.model : conv.model;
55
+ const subAgent = data.subAgent !== undefined ? data.subAgent : conv.subAgent;
56
+ const pinned = data.pinned !== undefined ? (data.pinned ? 1 : 0) : (conv.pinned || 0);
57
+ const tags = data.tags !== undefined ? (Array.isArray(data.tags) ? data.tags.join(',') : data.tags) : (conv.tags || null);
58
+ prep(`UPDATE conversations SET title = ?, status = ?, agentId = ?, agentType = ?, model = ?, subAgent = ?, pinned = ?, tags = ?, updated_at = ? WHERE id = ?`).run(title, status, agentId, agentType, model, subAgent, pinned, tags, now, id);
59
+ return { ...conv, title, status, agentId, agentType, model, subAgent, pinned, tags, updated_at: now };
60
+ },
61
+ setClaudeSessionId(conversationId, claudeSessionId, sessionId = null) {
62
+ prep('UPDATE conversations SET claudeSessionId = ?, updated_at = ? WHERE id = ?').run(claudeSessionId, Date.now(), conversationId);
63
+ if (sessionId) prep('UPDATE sessions SET claudeSessionId = ? WHERE id = ?').run(claudeSessionId, sessionId);
64
+ },
65
+ getClaudeSessionId(conversationId) { const row = prep('SELECT claudeSessionId FROM conversations WHERE id = ?').get(conversationId); return row?.claudeSessionId || null; },
66
+ setIsStreaming(conversationId, isStreaming) { prep('UPDATE conversations SET isStreaming = ?, updated_at = ? WHERE id = ?').run(isStreaming ? 1 : 0, Date.now(), conversationId); },
67
+ getIsStreaming(conversationId) { const row = prep('SELECT isStreaming FROM conversations WHERE id = ?').get(conversationId); return row?.isStreaming === 1; },
68
+ getStreamingConversations() { return prep('SELECT id, title, claudeSessionId, agentId, agentType, model, subAgent FROM conversations WHERE isStreaming = 1').all(); },
69
+ getResumableConversations(withinMs = 600000) {
70
+ const cutoff = Date.now() - withinMs;
71
+ return prep(`SELECT DISTINCT c.id, c.title, c.claudeSessionId, c.agentId, c.agentType, c.workingDirectory, c.model, c.subAgent FROM conversations c WHERE EXISTS (SELECT 1 FROM sessions s WHERE s.conversationId = c.id AND s.status IN ('active', 'pending', 'interrupted') AND s.started_at > ?)`).all(cutoff);
72
+ },
73
+ clearAllStreamingFlags() { return prep('UPDATE conversations SET isStreaming = 0 WHERE isStreaming = 1').run().changes; },
74
+ markSessionIncomplete(sessionId, errorMsg) { prep('UPDATE sessions SET status = ?, error = ?, completed_at = ? WHERE id = ?').run('incomplete', errorMsg || 'unknown', Date.now(), sessionId); },
75
+ getSessionsProcessingLongerThan(minutes) { const cutoff = Date.now() - (minutes * 60 * 1000); return prep("SELECT * FROM sessions WHERE status IN ('active', 'pending') AND started_at < ?").all(cutoff); },
76
+ cleanupOrphanedSessions(days) { const cutoff = Date.now() - (days * 24 * 60 * 60 * 1000); return prep("DELETE FROM sessions WHERE status IN ('active', 'pending') AND started_at < ?").run(cutoff).changes || 0; },
77
+ };
78
+
79
+ addMessageQueries(q, db, prep, generateId);
80
+ addSessionQueries(q, db, prep, generateId);
81
+ addEventQueries(q, db, prep, generateId);
82
+ addDeleteQueries(q, db, prep, generateId);
83
+ addCleanupQueries(q, db, prep, generateId);
84
+ addImportQueries(q, db, prep, generateId);
85
+ addStreamQueries(q, db, prep, generateId);
86
+ addChunkQueries(q, db, prep, generateId);
87
+ addChunkQueries2(q, db, prep, generateId);
88
+ addVoiceQueries(q, db, prep, generateId);
89
+ addToolQueries(q, db, prep, generateId);
90
+ Object.assign(q, createACPQueries(db, prep));
91
+ return q;
92
+ }