@devflow-tools/database 0.8.10 → 0.9.0

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/src/database.ts CHANGED
@@ -14,9 +14,9 @@ export class DevFlowDatabase {
14
14
  const dbPath = join(devflowDir, 'devflow.db');
15
15
  this.db = new NodeSqliteDatabase(dbPath);
16
16
 
17
- // 启用 WAL 模式
18
17
  this.db.exec('PRAGMA journal_mode = WAL');
19
18
  this.db.exec('PRAGMA busy_timeout = 5000');
19
+ this.db.exec('PRAGMA foreign_keys = OFF');
20
20
 
21
21
  this.initializeSchema();
22
22
  }
@@ -119,8 +119,7 @@ export class DevFlowDatabase {
119
119
  subagent_id TEXT,
120
120
  error TEXT,
121
121
  blocked INTEGER DEFAULT 0 CHECK (blocked IN (0, 1)),
122
- block_reason TEXT,
123
- FOREIGN KEY (execution_id) REFERENCES skill_executions(execution_id) ON DELETE CASCADE
122
+ block_reason TEXT
124
123
  );
125
124
 
126
125
  CREATE INDEX IF NOT EXISTS idx_executions_skill ON skill_executions(skill_name);
@@ -130,7 +129,82 @@ export class DevFlowDatabase {
130
129
  CREATE INDEX IF NOT EXISTS idx_events_timestamp ON tool_call_events(timestamp);
131
130
  CREATE INDEX IF NOT EXISTS idx_events_tool_type ON tool_call_events(tool_type);
132
131
  CREATE INDEX IF NOT EXISTS idx_events_mcp ON tool_call_events(is_mcp_tool);
132
+ CREATE INDEX IF NOT EXISTS idx_events_session ON tool_call_events(session_id);
133
+
134
+ -- Sessions table: top-level container per Claude Code session
135
+ CREATE TABLE IF NOT EXISTS sessions (
136
+ id TEXT PRIMARY KEY,
137
+ project_root TEXT NOT NULL,
138
+ label TEXT,
139
+ started_at INTEGER NOT NULL,
140
+ finished_at INTEGER,
141
+ duration_ms INTEGER DEFAULT 0,
142
+ total_tool_calls INTEGER DEFAULT 0,
143
+ mcp_tool_calls INTEGER DEFAULT 0,
144
+ direct_tool_calls INTEGER DEFAULT 0,
145
+ subagent_count INTEGER DEFAULT 0,
146
+ status TEXT NOT NULL DEFAULT 'running' CHECK (status IN ('running', 'completed', 'failed'))
147
+ );
148
+
149
+ CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_root);
150
+ CREATE INDEX IF NOT EXISTS idx_sessions_started ON sessions(started_at DESC);
133
151
  `);
152
+
153
+ // Migration: add session_id to skill_executions (SQLite compat — ignore if exists)
154
+ try { this.db.exec('ALTER TABLE skill_executions ADD COLUMN session_id TEXT REFERENCES sessions(id)'); } catch {}
155
+ // Migration: add session_id to tool_call_events
156
+ try { this.db.exec('ALTER TABLE tool_call_events ADD COLUMN session_id TEXT REFERENCES sessions(id)'); } catch {}
157
+ // Migration: kind column for event type classification
158
+ try { this.db.exec("ALTER TABLE tool_call_events ADD COLUMN kind TEXT DEFAULT 'tool_use'"); } catch {}
159
+ // Migration: workflow run persistence
160
+ try { this.db.exec('ALTER TABLE tool_call_events ADD COLUMN workflow_run_id TEXT'); } catch {}
161
+
162
+ // Migration: tool_metrics table for per-tool metrics collection
163
+ try {
164
+ this.db.exec(`
165
+ CREATE TABLE IF NOT EXISTS tool_metrics (
166
+ id TEXT PRIMARY KEY,
167
+ tool_call_event_id TEXT NOT NULL,
168
+ session_id TEXT NOT NULL,
169
+ tool_name TEXT NOT NULL,
170
+ query TEXT,
171
+ status TEXT NOT NULL DEFAULT 'success',
172
+ result_count INTEGER DEFAULT 0,
173
+ result_size_bytes INTEGER DEFAULT 0,
174
+ latency_ms INTEGER DEFAULT 0,
175
+ engine TEXT,
176
+ accuracy_query_id TEXT,
177
+ metadata TEXT,
178
+ created_at INTEGER NOT NULL
179
+ );
180
+
181
+ CREATE INDEX IF NOT EXISTS idx_tool_metrics_session ON tool_metrics(session_id);
182
+ CREATE INDEX IF NOT EXISTS idx_tool_metrics_tool ON tool_metrics(tool_name);
183
+ CREATE INDEX IF NOT EXISTS idx_tool_metrics_created ON tool_metrics(created_at);
184
+ `);
185
+ } catch {}
186
+
187
+ // Migration: accuracy queries table for search quality tracking
188
+ try {
189
+ this.db.exec(`
190
+ CREATE TABLE IF NOT EXISTS accuracy_queries (
191
+ id TEXT PRIMARY KEY,
192
+ session_id TEXT NOT NULL,
193
+ engine TEXT NOT NULL CHECK(engine IN ('codegraph','knowledge','memory')),
194
+ query TEXT NOT NULL,
195
+ top_k_results TEXT NOT NULL,
196
+ selected_ids TEXT,
197
+ relevance_feedback TEXT CHECK(relevance_feedback IN ('hit','partial','miss')),
198
+ annotator_note TEXT,
199
+ annotated_at INTEGER,
200
+ created_at INTEGER NOT NULL
201
+ );
202
+
203
+ CREATE INDEX IF NOT EXISTS idx_accuracy_queries_engine ON accuracy_queries(engine);
204
+ CREATE INDEX IF NOT EXISTS idx_accuracy_queries_session ON accuracy_queries(session_id);
205
+ CREATE INDEX IF NOT EXISTS idx_accuracy_queries_feedback ON accuracy_queries(relevance_feedback);
206
+ `);
207
+ } catch {}
134
208
  }
135
209
 
136
210
  insertRun(run: any): void {
@@ -213,14 +287,14 @@ export class DevFlowDatabase {
213
287
  INSERT INTO telemetry_events (run_id, kind, timestamp, duration, success, tool_name, plugin_name, metadata)
214
288
  VALUES (?, ?, ?, ?, ?, ?, ?, ?)
215
289
  `).run(
216
- event.runId,
217
- event.kind,
218
- event.timestamp,
219
- event.duration,
220
- event.success,
221
- event.toolName,
222
- event.pluginName,
223
- JSON.stringify(event.metadata) ?? null
290
+ event.runId ?? null,
291
+ event.kind ?? null,
292
+ event.timestamp ?? null,
293
+ event.duration ?? null,
294
+ event.success ?? null,
295
+ event.toolName ?? null,
296
+ event.pluginName ?? null,
297
+ event.metadata ? JSON.stringify(event.metadata) : null
224
298
  );
225
299
  }
226
300
 
@@ -290,10 +364,165 @@ export class DevFlowDatabase {
290
364
  this.db.prepare('DELETE FROM settings WHERE key = ?').run(key);
291
365
  }
292
366
 
367
+ // ---- Sessions ----
368
+
369
+ insertSession(params: {
370
+ id: string;
371
+ projectRoot: string;
372
+ label?: string;
373
+ startedAt: number;
374
+ }): void {
375
+ this.db.prepare(`
376
+ INSERT OR IGNORE INTO sessions (id, project_root, label, started_at, status)
377
+ VALUES (?, ?, ?, ?, 'running')
378
+ `).run(params.id, params.projectRoot, params.label ?? null, params.startedAt);
379
+ }
380
+
381
+ getSession(id: string): any | null {
382
+ const row = this.db.prepare('SELECT * FROM sessions WHERE id = ?').get(id) as any;
383
+ if (!row) return null;
384
+ return {
385
+ id: row.id,
386
+ projectRoot: row.project_root,
387
+ label: row.label,
388
+ startedAt: row.started_at,
389
+ finishedAt: row.finished_at,
390
+ durationMs: row.duration_ms,
391
+ totalToolCalls: row.total_tool_calls,
392
+ mcpToolCalls: row.mcp_tool_calls,
393
+ directToolCalls: row.direct_tool_calls,
394
+ subagentCount: row.subagent_count,
395
+ status: row.status,
396
+ };
397
+ }
398
+
399
+ listSessions(limit: number, offset: number, projectRoot?: string): any[] {
400
+ let query = 'SELECT * FROM sessions';
401
+ const params: any[] = [];
402
+ if (projectRoot) {
403
+ query += ' WHERE project_root = ?';
404
+ params.push(projectRoot);
405
+ }
406
+ query += ' ORDER BY started_at DESC LIMIT ? OFFSET ?';
407
+ params.push(Number(limit), Number(offset));
408
+
409
+ const rows = this.db.prepare(query).all(...params) as any[];
410
+ return rows.map((row: any) => ({
411
+ id: row.id,
412
+ projectRoot: row.project_root,
413
+ label: row.label,
414
+ startedAt: row.started_at,
415
+ finishedAt: row.finished_at,
416
+ durationMs: row.duration_ms,
417
+ totalToolCalls: row.total_tool_calls,
418
+ mcpToolCalls: row.mcp_tool_calls,
419
+ directToolCalls: row.direct_tool_calls,
420
+ subagentCount: row.subagent_count,
421
+ status: row.status,
422
+ }));
423
+ }
424
+
425
+ closeSession(id: string, finishedAt?: number): void {
426
+ const events = this.db.prepare(
427
+ 'SELECT * FROM tool_call_events WHERE session_id = ? OR execution_id = ?'
428
+ ).all(id, id) as any[];
429
+
430
+ // deduplicate by event_id
431
+ const seen = new Set<string>();
432
+ const unique = events.filter((e: any) => {
433
+ if (seen.has(e.event_id)) return false;
434
+ seen.add(e.event_id);
435
+ return true;
436
+ });
437
+
438
+ const totalToolCalls = unique.length;
439
+ const mcpToolCalls = unique.filter((e: any) => e.is_mcp_tool === 1).length;
440
+ const directToolCalls = totalToolCalls - mcpToolCalls;
441
+ const subagentCount = unique.filter((e: any) => e.tool_type === 'subagent').length;
442
+
443
+ const timestamps = unique.map((e: any) => e.timestamp).filter(Boolean) as number[];
444
+ const durationMs = timestamps.length >= 2
445
+ ? Math.max(...timestamps) - Math.min(...timestamps)
446
+ : 0;
447
+
448
+ this.db.prepare(`
449
+ UPDATE sessions
450
+ SET finished_at = ?, duration_ms = ?, total_tool_calls = ?,
451
+ mcp_tool_calls = ?, direct_tool_calls = ?, subagent_count = ?,
452
+ status = 'completed'
453
+ WHERE id = ?
454
+ `).run(finishedAt ?? Date.now(), durationMs, totalToolCalls, mcpToolCalls, directToolCalls, subagentCount, id);
455
+ }
456
+
457
+ listToolCallEventsBySession(sessionId: string): any[] {
458
+ const rows = this.db.prepare(
459
+ 'SELECT * FROM tool_call_events WHERE session_id = ? ORDER BY timestamp ASC'
460
+ ).all(sessionId) as any[];
461
+
462
+ return rows.map(row => ({
463
+ eventId: row.event_id,
464
+ executionId: row.execution_id,
465
+ sessionId: row.session_id,
466
+ timestamp: row.timestamp,
467
+ toolName: row.tool_name,
468
+ toolType: row.tool_type,
469
+ isMcpTool: row.is_mcp_tool === 1,
470
+ mcpToolName: row.mcp_tool_name,
471
+ mcpEnforced: row.mcp_enforced === 1,
472
+ mcpFallback: row.mcp_fallback === 1,
473
+ kind: row.kind,
474
+ input: row.input ? JSON.parse(row.input) : null,
475
+ output: row.output ? JSON.parse(row.output) : null,
476
+ duration: row.duration,
477
+ parentToolCallId: row.parent_tool_call_id,
478
+ subagentId: row.subagent_id,
479
+ error: row.error,
480
+ blocked: row.blocked === 1,
481
+ blockReason: row.block_reason,
482
+ }));
483
+ }
484
+
485
+ listToolCallEventsBySessions(sessionIds: string[]): Record<string, any[]> {
486
+ if (sessionIds.length === 0) return {};
487
+ const placeholders = sessionIds.map(() => '?').join(',');
488
+ const rows = this.db.prepare(
489
+ `SELECT * FROM tool_call_events WHERE session_id IN (${placeholders}) ORDER BY timestamp ASC`
490
+ ).all(...sessionIds) as any[];
491
+
492
+ const grouped: Record<string, any[]> = {};
493
+ for (const row of rows) {
494
+ const sid = row.session_id as string;
495
+ if (!grouped[sid]) grouped[sid] = [];
496
+ grouped[sid].push({
497
+ eventId: row.event_id,
498
+ executionId: row.execution_id,
499
+ sessionId: row.session_id,
500
+ timestamp: row.timestamp,
501
+ toolName: row.tool_name,
502
+ toolType: row.tool_type,
503
+ isMcpTool: row.is_mcp_tool === 1,
504
+ mcpToolName: row.mcp_tool_name,
505
+ mcpEnforced: row.mcp_enforced === 1,
506
+ mcpFallback: row.mcp_fallback === 1,
507
+ kind: row.kind,
508
+ input: row.input ? JSON.parse(row.input) : null,
509
+ output: row.output ? JSON.parse(row.output) : null,
510
+ duration: row.duration,
511
+ parentToolCallId: row.parent_tool_call_id,
512
+ subagentId: row.subagent_id,
513
+ error: row.error,
514
+ blocked: row.blocked === 1,
515
+ blockReason: row.block_reason,
516
+ });
517
+ }
518
+ return grouped;
519
+ }
520
+
293
521
  // ---- Skill Executions ----
294
522
 
295
523
  insertSkillExecution(params: {
296
524
  executionId: string;
525
+ sessionId?: string;
297
526
  skillName: string;
298
527
  startedAt: number;
299
528
  status: string;
@@ -302,10 +531,11 @@ export class DevFlowDatabase {
302
531
  }): void {
303
532
  this.db.prepare(`
304
533
  INSERT INTO skill_executions
305
- (execution_id, skill_name, started_at, status, required_mcp_tools, available_mcp_tools)
306
- VALUES (?, ?, ?, ?, ?, ?)
534
+ (execution_id, session_id, skill_name, started_at, status, required_mcp_tools, available_mcp_tools)
535
+ VALUES (?, ?, ?, ?, ?, ?, ?)
307
536
  `).run(
308
537
  params.executionId,
538
+ params.sessionId ?? null,
309
539
  params.skillName,
310
540
  params.startedAt,
311
541
  params.status,
@@ -319,6 +549,7 @@ export class DevFlowDatabase {
319
549
  if (!row) return null;
320
550
  return {
321
551
  executionId: row.execution_id,
552
+ sessionId: row.session_id,
322
553
  skillName: row.skill_name,
323
554
  startedAt: row.started_at,
324
555
  finishedAt: row.finished_at,
@@ -329,7 +560,6 @@ export class DevFlowDatabase {
329
560
  mcpToolCalls: row.mcp_tool_calls,
330
561
  directToolCalls: row.direct_tool_calls,
331
562
  subagentCount: row.subagent_count,
332
- totalTokens: row.total_tokens,
333
563
  totalDuration: row.total_duration,
334
564
  mcpComplianceRate: row.mcp_compliance_rate,
335
565
  missedMcpTools: row.missed_mcp_tools ? JSON.parse(row.missed_mcp_tools) : null,
@@ -341,7 +571,6 @@ export class DevFlowDatabase {
341
571
  status?: string;
342
572
  finishedAt?: number;
343
573
  totalDuration?: number;
344
- totalTokens?: number;
345
574
  mcpComplianceRate?: number;
346
575
  missedMcpTools?: string[];
347
576
  totalToolCalls?: number;
@@ -355,7 +584,6 @@ export class DevFlowDatabase {
355
584
  if (updates.status !== undefined) { sets.push('status = ?'); values.push(updates.status); }
356
585
  if (updates.finishedAt !== undefined) { sets.push('finished_at = ?'); values.push(updates.finishedAt); }
357
586
  if (updates.totalDuration !== undefined) { sets.push('total_duration = ?'); values.push(updates.totalDuration); }
358
- if (updates.totalTokens !== undefined) { sets.push('total_tokens = ?'); values.push(updates.totalTokens); }
359
587
  if (updates.mcpComplianceRate !== undefined) { sets.push('mcp_compliance_rate = ?'); values.push(updates.mcpComplianceRate); }
360
588
  if (updates.missedMcpTools !== undefined) { sets.push('missed_mcp_tools = ?'); values.push(JSON.stringify(updates.missedMcpTools)); }
361
589
  if (updates.totalToolCalls !== undefined) { sets.push('total_tool_calls = ?'); values.push(updates.totalToolCalls); }
@@ -370,14 +598,6 @@ export class DevFlowDatabase {
370
598
  }
371
599
 
372
600
  listSkillExecutions(limit: number, skillName?: string): any[] {
373
- // Auto-timeout stale running executions (> 30 minutes)
374
- const timeoutThreshold = Date.now() - 30 * 60 * 1000;
375
- this.db.prepare(`
376
- UPDATE skill_executions
377
- SET status = 'failed', finished_at = ?
378
- WHERE status = 'running' AND started_at < ?
379
- `).run(Date.now(), timeoutThreshold);
380
-
381
601
  let query = 'SELECT * FROM skill_executions';
382
602
  const params: any[] = [];
383
603
 
@@ -392,6 +612,7 @@ export class DevFlowDatabase {
392
612
  const rows = this.db.prepare(query).all(...params) as any[];
393
613
  return rows.map(row => ({
394
614
  executionId: row.execution_id,
615
+ sessionId: row.session_id,
395
616
  skillName: row.skill_name,
396
617
  startedAt: row.started_at,
397
618
  finishedAt: row.finished_at,
@@ -400,7 +621,6 @@ export class DevFlowDatabase {
400
621
  mcpToolCalls: row.mcp_tool_calls,
401
622
  directToolCalls: row.direct_tool_calls,
402
623
  subagentCount: row.subagent_count,
403
- totalTokens: row.total_tokens,
404
624
  totalDuration: row.total_duration,
405
625
  mcpComplianceRate: row.mcp_compliance_rate,
406
626
  }));
@@ -411,6 +631,7 @@ export class DevFlowDatabase {
411
631
  insertToolCallEvent(params: {
412
632
  eventId: string;
413
633
  executionId: string;
634
+ sessionId?: string;
414
635
  timestamp: number;
415
636
  toolName: string;
416
637
  toolType: string;
@@ -418,25 +639,27 @@ export class DevFlowDatabase {
418
639
  mcpToolName?: string;
419
640
  mcpEnforced: boolean;
420
641
  mcpFallback: boolean;
642
+ kind?: string;
421
643
  input: unknown;
422
644
  output?: unknown;
423
- tokensUsed: number;
424
645
  duration: number;
425
646
  parentToolCallId?: string;
426
647
  subagentId?: string;
427
648
  error?: string;
428
649
  blocked: boolean;
429
650
  blockReason?: string;
651
+ workflowRunId?: string;
430
652
  }): void {
431
653
  this.db.prepare(`
432
654
  INSERT INTO tool_call_events
433
- (event_id, execution_id, timestamp, tool_name, tool_type, is_mcp_tool,
434
- mcp_tool_name, mcp_enforced, mcp_fallback, input, output, tokens_used, duration,
435
- parent_tool_call_id, subagent_id, error, blocked, block_reason)
436
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
655
+ (event_id, execution_id, session_id, timestamp, tool_name, tool_type, is_mcp_tool,
656
+ mcp_tool_name, mcp_enforced, mcp_fallback, kind, input, output, duration,
657
+ parent_tool_call_id, subagent_id, error, blocked, block_reason, workflow_run_id)
658
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
437
659
  `).run(
438
660
  params.eventId,
439
- params.executionId,
661
+ params.executionId || null,
662
+ params.sessionId ?? null,
440
663
  params.timestamp,
441
664
  params.toolName,
442
665
  params.toolType,
@@ -444,15 +667,16 @@ export class DevFlowDatabase {
444
667
  params.mcpToolName || null,
445
668
  params.mcpEnforced ? 1 : 0,
446
669
  params.mcpFallback ? 1 : 0,
670
+ params.kind ?? 'tool_use',
447
671
  JSON.stringify(params.input) ?? null,
448
672
  params.output ? JSON.stringify(params.output) : null,
449
- params.tokensUsed,
450
673
  params.duration,
451
674
  params.parentToolCallId || null,
452
675
  params.subagentId || null,
453
676
  params.error || null,
454
677
  params.blocked ? 1 : 0,
455
678
  params.blockReason || null,
679
+ params.workflowRunId || null,
456
680
  );
457
681
 
458
682
  // Update execution summary counters
@@ -479,6 +703,7 @@ export class DevFlowDatabase {
479
703
  return rows.map(row => ({
480
704
  eventId: row.event_id,
481
705
  executionId: row.execution_id,
706
+ sessionId: row.session_id,
482
707
  timestamp: row.timestamp,
483
708
  toolName: row.tool_name,
484
709
  toolType: row.tool_type,
@@ -486,9 +711,9 @@ export class DevFlowDatabase {
486
711
  mcpToolName: row.mcp_tool_name,
487
712
  mcpEnforced: row.mcp_enforced === 1,
488
713
  mcpFallback: row.mcp_fallback === 1,
714
+ kind: row.kind,
489
715
  input: row.input ? JSON.parse(row.input) : null,
490
716
  output: row.output ? JSON.parse(row.output) : null,
491
- tokensUsed: row.tokens_used,
492
717
  duration: row.duration,
493
718
  parentToolCallId: row.parent_tool_call_id,
494
719
  subagentId: row.subagent_id,
@@ -498,7 +723,7 @@ export class DevFlowDatabase {
498
723
  }));
499
724
  }
500
725
 
501
- updateToolCallEvent(eventId: string, updates: { output?: string; error?: string }): void {
726
+ updateToolCallEvent(eventId: string, updates: { output?: string; error?: string; duration?: number }): void {
502
727
  const sets: string[] = [];
503
728
  const values: any[] = [];
504
729
 
@@ -510,6 +735,10 @@ export class DevFlowDatabase {
510
735
  sets.push('error = ?');
511
736
  values.push(updates.error);
512
737
  }
738
+ if (updates.duration !== undefined) {
739
+ sets.push('duration = ?');
740
+ values.push(updates.duration);
741
+ }
513
742
 
514
743
  if (sets.length > 0) {
515
744
  values.push(eventId);
@@ -569,6 +798,145 @@ export class DevFlowDatabase {
569
798
  return { overall, bySkill, missedTools };
570
799
  }
571
800
 
801
+ cleanupStaleRecords(): void {
802
+ const cutoff = Date.now() - 30 * 60 * 1000;
803
+ try {
804
+ this.db.prepare(
805
+ `UPDATE skill_executions SET status = 'failed', finished_at = ? WHERE status = 'running' AND started_at < ?`
806
+ ).run(Date.now(), cutoff);
807
+ this.db.prepare(
808
+ `UPDATE sessions SET status = 'failed', finished_at = ? WHERE status = 'running' AND started_at < ?`
809
+ ).run(Date.now(), cutoff);
810
+ } catch { /* best-effort */ }
811
+ }
812
+
813
+ insertToolMetric(metric: {
814
+ id: string;
815
+ toolCallEventId: string;
816
+ sessionId: string;
817
+ toolName: string;
818
+ query?: string;
819
+ status?: string;
820
+ resultCount?: number;
821
+ resultSizeBytes?: number;
822
+ latencyMs?: number;
823
+ engine?: string;
824
+ accuracyQueryId?: string;
825
+ metadata?: string;
826
+ createdAt: number;
827
+ }): void {
828
+ this.db.prepare(`
829
+ INSERT OR REPLACE INTO tool_metrics (id, tool_call_event_id, session_id, tool_name, query, status, result_count, result_size_bytes, latency_ms, engine, accuracy_query_id, metadata, created_at)
830
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
831
+ `).run(
832
+ metric.id, metric.toolCallEventId, metric.sessionId, metric.toolName,
833
+ metric.query ?? null, metric.status ?? 'success',
834
+ metric.resultCount ?? 0, metric.resultSizeBytes ?? 0,
835
+ metric.latencyMs ?? 0, metric.engine ?? null,
836
+ metric.accuracyQueryId ?? null, metric.metadata ?? null, metric.createdAt,
837
+ );
838
+ }
839
+
840
+ getToolMetrics(sessionId?: string, toolName?: string, limit = 100, offset = 0): any[] {
841
+ let sql = 'SELECT * FROM tool_metrics WHERE 1=1';
842
+ const params: any[] = [];
843
+ if (sessionId) { sql += ' AND session_id = ?'; params.push(sessionId); }
844
+ if (toolName) { sql += ' AND tool_name = ?'; params.push(toolName); }
845
+ sql += ' ORDER BY created_at DESC LIMIT ? OFFSET ?';
846
+ params.push(limit, offset);
847
+ return (this.db.prepare(sql).all(...params) as any[]);
848
+ }
849
+
850
+ getToolMetricsSummary(days = 7): any[] {
851
+ const cutoff = Date.now() - days * 24 * 60 * 60 * 1000;
852
+ const rows = (this.db.prepare(`
853
+ SELECT tool_name, COUNT(*) as call_count,
854
+ SUM(CASE WHEN status = 'success' THEN 1 ELSE 0 END) as success_count,
855
+ SUM(CASE WHEN result_count = 0 THEN 1 ELSE 0 END) as empty_count,
856
+ AVG(result_count) as avg_results,
857
+ AVG(latency_ms) as avg_latency
858
+ FROM tool_metrics
859
+ WHERE created_at >= ?
860
+ GROUP BY tool_name
861
+ ORDER BY call_count DESC
862
+ `).all(cutoff) as any[]);
863
+ return rows.map((r: any) => ({
864
+ toolName: r.tool_name,
865
+ callCount: r.call_count,
866
+ successRate: r.call_count > 0 ? Math.round((r.success_count / r.call_count) * 10000) / 100 : 0,
867
+ emptyRate: r.call_count > 0 ? Math.round((r.empty_count / r.call_count) * 10000) / 100 : 0,
868
+ avgResults: Math.round(r.avg_results * 10) / 10,
869
+ avgLatency: Math.round(r.avg_latency),
870
+ }));
871
+ }
872
+
873
+ // ---- Accuracy Queries ----
874
+
875
+ insertAccuracyQuery(q: {
876
+ id: string; sessionId: string; engine: string; query: string;
877
+ topKResults: string; selectedIds?: string; createdAt: number;
878
+ }): void {
879
+ this.db.prepare(`
880
+ INSERT INTO accuracy_queries (id, session_id, engine, query, top_k_results, selected_ids, created_at)
881
+ VALUES (?, ?, ?, ?, ?, ?, ?)
882
+ `).run(q.id, q.sessionId, q.engine, q.query, q.topKResults, q.selectedIds ?? null, q.createdAt);
883
+ }
884
+
885
+ getAccuracyQueries(options: { engine?: string; status?: string; limit?: number; offset?: number }): any[] {
886
+ let sql = 'SELECT * FROM accuracy_queries WHERE 1=1';
887
+ const params: any[] = [];
888
+ if (options.engine) { sql += ' AND engine = ?'; params.push(options.engine); }
889
+ if (options.status === 'annotated') sql += ' AND relevance_feedback IS NOT NULL';
890
+ if (options.status === 'unannotated') sql += ' AND relevance_feedback IS NULL';
891
+ sql += ' ORDER BY created_at DESC LIMIT ? OFFSET ?';
892
+ params.push(options.limit ?? 50, options.offset ?? 0);
893
+ const rows = (this.db.prepare(sql).all(...params) as any[]);
894
+ return rows.map((r: any) => ({
895
+ id: r.id, sessionId: r.session_id, engine: r.engine, query: r.query,
896
+ topKResults: r.top_k_results ? JSON.parse(r.top_k_results) : [],
897
+ selectedIds: r.selected_ids ? JSON.parse(r.selected_ids) : null,
898
+ relevanceFeedback: r.relevance_feedback,
899
+ annotatorNote: r.annotator_note,
900
+ annotatedAt: r.annotated_at,
901
+ createdAt: r.created_at,
902
+ }));
903
+ }
904
+
905
+ updateAccuracyFeedback(id: string, feedback: { relevance: 'hit' | 'partial' | 'miss'; note?: string }): void {
906
+ this.db.prepare(
907
+ 'UPDATE accuracy_queries SET relevance_feedback = ?, annotator_note = ?, annotated_at = ? WHERE id = ?'
908
+ ).run(feedback.relevance, feedback.note ?? null, Date.now(), id);
909
+ }
910
+
911
+ getAccuracyStats(engine?: string, since?: number): any {
912
+ let where = 'WHERE 1=1';
913
+ const params: any[] = [];
914
+ if (engine) { where += ' AND engine = ?'; params.push(engine); }
915
+ if (since) { where += ' AND created_at >= ?'; params.push(since); }
916
+
917
+ const totalQueries = (this.db.prepare(`SELECT COUNT(*) as c FROM accuracy_queries ${where}`).get(...params) as any)?.c ?? 0;
918
+ const annotatedQueries = (this.db.prepare(
919
+ `SELECT COUNT(*) as c FROM accuracy_queries ${where} AND relevance_feedback IS NOT NULL`
920
+ ).get(...params) as any)?.c ?? 0;
921
+ const hitCount = annotatedQueries > 0
922
+ ? ((this.db.prepare(
923
+ `SELECT COUNT(*) as c FROM accuracy_queries ${where} AND relevance_feedback = 'hit'`
924
+ ).get(...params) as any)?.c ?? 0)
925
+ : 0;
926
+ const hitRate = annotatedQueries > 0 ? hitCount / annotatedQueries : null;
927
+
928
+ return { totalQueries, annotatedQueries, hitRate };
929
+ }
930
+
931
+ // Convenience methods for raw SQL queries (used by AutoChecker)
932
+ all(sql: string, ...params: unknown[]): unknown[] {
933
+ return this.db.prepare(sql).all(...params);
934
+ }
935
+
936
+ get(sql: string, ...params: unknown[]): unknown {
937
+ return this.db.prepare(sql).get(...params);
938
+ }
939
+
572
940
  close(): void {
573
941
  this.db.close();
574
942
  }