@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/CHANGELOG.md +36 -0
- package/README.md +68 -0
- package/dist/database.d.ts +56 -2
- package/dist/database.js +349 -26
- package/package.json +2 -2
- package/src/database.ts +402 -34
- package/tsconfig.tsbuildinfo +1 -1
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)
|
|
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,
|
|
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
|
}
|