@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/dist/database.js
CHANGED
|
@@ -12,9 +12,9 @@ class DevFlowDatabase {
|
|
|
12
12
|
}
|
|
13
13
|
const dbPath = (0, path_1.join)(devflowDir, 'devflow.db');
|
|
14
14
|
this.db = new node_sqlite_1.NodeSqliteDatabase(dbPath);
|
|
15
|
-
// 启用 WAL 模式
|
|
16
15
|
this.db.exec('PRAGMA journal_mode = WAL');
|
|
17
16
|
this.db.exec('PRAGMA busy_timeout = 5000');
|
|
17
|
+
this.db.exec('PRAGMA foreign_keys = OFF');
|
|
18
18
|
this.initializeSchema();
|
|
19
19
|
}
|
|
20
20
|
initializeSchema() {
|
|
@@ -115,8 +115,7 @@ class DevFlowDatabase {
|
|
|
115
115
|
subagent_id TEXT,
|
|
116
116
|
error TEXT,
|
|
117
117
|
blocked INTEGER DEFAULT 0 CHECK (blocked IN (0, 1)),
|
|
118
|
-
block_reason TEXT
|
|
119
|
-
FOREIGN KEY (execution_id) REFERENCES skill_executions(execution_id) ON DELETE CASCADE
|
|
118
|
+
block_reason TEXT
|
|
120
119
|
);
|
|
121
120
|
|
|
122
121
|
CREATE INDEX IF NOT EXISTS idx_executions_skill ON skill_executions(skill_name);
|
|
@@ -126,7 +125,93 @@ class DevFlowDatabase {
|
|
|
126
125
|
CREATE INDEX IF NOT EXISTS idx_events_timestamp ON tool_call_events(timestamp);
|
|
127
126
|
CREATE INDEX IF NOT EXISTS idx_events_tool_type ON tool_call_events(tool_type);
|
|
128
127
|
CREATE INDEX IF NOT EXISTS idx_events_mcp ON tool_call_events(is_mcp_tool);
|
|
128
|
+
CREATE INDEX IF NOT EXISTS idx_events_session ON tool_call_events(session_id);
|
|
129
|
+
|
|
130
|
+
-- Sessions table: top-level container per Claude Code session
|
|
131
|
+
CREATE TABLE IF NOT EXISTS sessions (
|
|
132
|
+
id TEXT PRIMARY KEY,
|
|
133
|
+
project_root TEXT NOT NULL,
|
|
134
|
+
label TEXT,
|
|
135
|
+
started_at INTEGER NOT NULL,
|
|
136
|
+
finished_at INTEGER,
|
|
137
|
+
duration_ms INTEGER DEFAULT 0,
|
|
138
|
+
total_tool_calls INTEGER DEFAULT 0,
|
|
139
|
+
mcp_tool_calls INTEGER DEFAULT 0,
|
|
140
|
+
direct_tool_calls INTEGER DEFAULT 0,
|
|
141
|
+
subagent_count INTEGER DEFAULT 0,
|
|
142
|
+
status TEXT NOT NULL DEFAULT 'running' CHECK (status IN ('running', 'completed', 'failed'))
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_root);
|
|
146
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_started ON sessions(started_at DESC);
|
|
129
147
|
`);
|
|
148
|
+
// Migration: add session_id to skill_executions (SQLite compat — ignore if exists)
|
|
149
|
+
try {
|
|
150
|
+
this.db.exec('ALTER TABLE skill_executions ADD COLUMN session_id TEXT REFERENCES sessions(id)');
|
|
151
|
+
}
|
|
152
|
+
catch { }
|
|
153
|
+
// Migration: add session_id to tool_call_events
|
|
154
|
+
try {
|
|
155
|
+
this.db.exec('ALTER TABLE tool_call_events ADD COLUMN session_id TEXT REFERENCES sessions(id)');
|
|
156
|
+
}
|
|
157
|
+
catch { }
|
|
158
|
+
// Migration: kind column for event type classification
|
|
159
|
+
try {
|
|
160
|
+
this.db.exec("ALTER TABLE tool_call_events ADD COLUMN kind TEXT DEFAULT 'tool_use'");
|
|
161
|
+
}
|
|
162
|
+
catch { }
|
|
163
|
+
// Migration: workflow run persistence
|
|
164
|
+
try {
|
|
165
|
+
this.db.exec('ALTER TABLE tool_call_events ADD COLUMN workflow_run_id TEXT');
|
|
166
|
+
}
|
|
167
|
+
catch { }
|
|
168
|
+
// Migration: tool_metrics table for per-tool metrics collection
|
|
169
|
+
try {
|
|
170
|
+
this.db.exec(`
|
|
171
|
+
CREATE TABLE IF NOT EXISTS tool_metrics (
|
|
172
|
+
id TEXT PRIMARY KEY,
|
|
173
|
+
tool_call_event_id TEXT NOT NULL,
|
|
174
|
+
session_id TEXT NOT NULL,
|
|
175
|
+
tool_name TEXT NOT NULL,
|
|
176
|
+
query TEXT,
|
|
177
|
+
status TEXT NOT NULL DEFAULT 'success',
|
|
178
|
+
result_count INTEGER DEFAULT 0,
|
|
179
|
+
result_size_bytes INTEGER DEFAULT 0,
|
|
180
|
+
latency_ms INTEGER DEFAULT 0,
|
|
181
|
+
engine TEXT,
|
|
182
|
+
accuracy_query_id TEXT,
|
|
183
|
+
metadata TEXT,
|
|
184
|
+
created_at INTEGER NOT NULL
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
CREATE INDEX IF NOT EXISTS idx_tool_metrics_session ON tool_metrics(session_id);
|
|
188
|
+
CREATE INDEX IF NOT EXISTS idx_tool_metrics_tool ON tool_metrics(tool_name);
|
|
189
|
+
CREATE INDEX IF NOT EXISTS idx_tool_metrics_created ON tool_metrics(created_at);
|
|
190
|
+
`);
|
|
191
|
+
}
|
|
192
|
+
catch { }
|
|
193
|
+
// Migration: accuracy queries table for search quality tracking
|
|
194
|
+
try {
|
|
195
|
+
this.db.exec(`
|
|
196
|
+
CREATE TABLE IF NOT EXISTS accuracy_queries (
|
|
197
|
+
id TEXT PRIMARY KEY,
|
|
198
|
+
session_id TEXT NOT NULL,
|
|
199
|
+
engine TEXT NOT NULL CHECK(engine IN ('codegraph','knowledge','memory')),
|
|
200
|
+
query TEXT NOT NULL,
|
|
201
|
+
top_k_results TEXT NOT NULL,
|
|
202
|
+
selected_ids TEXT,
|
|
203
|
+
relevance_feedback TEXT CHECK(relevance_feedback IN ('hit','partial','miss')),
|
|
204
|
+
annotator_note TEXT,
|
|
205
|
+
annotated_at INTEGER,
|
|
206
|
+
created_at INTEGER NOT NULL
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
CREATE INDEX IF NOT EXISTS idx_accuracy_queries_engine ON accuracy_queries(engine);
|
|
210
|
+
CREATE INDEX IF NOT EXISTS idx_accuracy_queries_session ON accuracy_queries(session_id);
|
|
211
|
+
CREATE INDEX IF NOT EXISTS idx_accuracy_queries_feedback ON accuracy_queries(relevance_feedback);
|
|
212
|
+
`);
|
|
213
|
+
}
|
|
214
|
+
catch { }
|
|
130
215
|
}
|
|
131
216
|
insertRun(run) {
|
|
132
217
|
this.db.prepare(`
|
|
@@ -181,7 +266,7 @@ class DevFlowDatabase {
|
|
|
181
266
|
this.db.prepare(`
|
|
182
267
|
INSERT INTO telemetry_events (run_id, kind, timestamp, duration, success, tool_name, plugin_name, metadata)
|
|
183
268
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
184
|
-
`).run(event.runId, event.kind, event.timestamp, event.duration, event.success, event.toolName, event.pluginName, JSON.stringify(event.metadata)
|
|
269
|
+
`).run(event.runId ?? null, event.kind ?? null, event.timestamp ?? null, event.duration ?? null, event.success ?? null, event.toolName ?? null, event.pluginName ?? null, event.metadata ? JSON.stringify(event.metadata) : null);
|
|
185
270
|
}
|
|
186
271
|
getEvents(runId) {
|
|
187
272
|
const rows = this.db.prepare('SELECT * FROM telemetry_events WHERE run_id = ?').all(runId);
|
|
@@ -234,13 +319,146 @@ class DevFlowDatabase {
|
|
|
234
319
|
deleteSetting(key) {
|
|
235
320
|
this.db.prepare('DELETE FROM settings WHERE key = ?').run(key);
|
|
236
321
|
}
|
|
322
|
+
// ---- Sessions ----
|
|
323
|
+
insertSession(params) {
|
|
324
|
+
this.db.prepare(`
|
|
325
|
+
INSERT OR IGNORE INTO sessions (id, project_root, label, started_at, status)
|
|
326
|
+
VALUES (?, ?, ?, ?, 'running')
|
|
327
|
+
`).run(params.id, params.projectRoot, params.label ?? null, params.startedAt);
|
|
328
|
+
}
|
|
329
|
+
getSession(id) {
|
|
330
|
+
const row = this.db.prepare('SELECT * FROM sessions WHERE id = ?').get(id);
|
|
331
|
+
if (!row)
|
|
332
|
+
return null;
|
|
333
|
+
return {
|
|
334
|
+
id: row.id,
|
|
335
|
+
projectRoot: row.project_root,
|
|
336
|
+
label: row.label,
|
|
337
|
+
startedAt: row.started_at,
|
|
338
|
+
finishedAt: row.finished_at,
|
|
339
|
+
durationMs: row.duration_ms,
|
|
340
|
+
totalToolCalls: row.total_tool_calls,
|
|
341
|
+
mcpToolCalls: row.mcp_tool_calls,
|
|
342
|
+
directToolCalls: row.direct_tool_calls,
|
|
343
|
+
subagentCount: row.subagent_count,
|
|
344
|
+
status: row.status,
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
listSessions(limit, offset, projectRoot) {
|
|
348
|
+
let query = 'SELECT * FROM sessions';
|
|
349
|
+
const params = [];
|
|
350
|
+
if (projectRoot) {
|
|
351
|
+
query += ' WHERE project_root = ?';
|
|
352
|
+
params.push(projectRoot);
|
|
353
|
+
}
|
|
354
|
+
query += ' ORDER BY started_at DESC LIMIT ? OFFSET ?';
|
|
355
|
+
params.push(Number(limit), Number(offset));
|
|
356
|
+
const rows = this.db.prepare(query).all(...params);
|
|
357
|
+
return rows.map((row) => ({
|
|
358
|
+
id: row.id,
|
|
359
|
+
projectRoot: row.project_root,
|
|
360
|
+
label: row.label,
|
|
361
|
+
startedAt: row.started_at,
|
|
362
|
+
finishedAt: row.finished_at,
|
|
363
|
+
durationMs: row.duration_ms,
|
|
364
|
+
totalToolCalls: row.total_tool_calls,
|
|
365
|
+
mcpToolCalls: row.mcp_tool_calls,
|
|
366
|
+
directToolCalls: row.direct_tool_calls,
|
|
367
|
+
subagentCount: row.subagent_count,
|
|
368
|
+
status: row.status,
|
|
369
|
+
}));
|
|
370
|
+
}
|
|
371
|
+
closeSession(id, finishedAt) {
|
|
372
|
+
const events = this.db.prepare('SELECT * FROM tool_call_events WHERE session_id = ? OR execution_id = ?').all(id, id);
|
|
373
|
+
// deduplicate by event_id
|
|
374
|
+
const seen = new Set();
|
|
375
|
+
const unique = events.filter((e) => {
|
|
376
|
+
if (seen.has(e.event_id))
|
|
377
|
+
return false;
|
|
378
|
+
seen.add(e.event_id);
|
|
379
|
+
return true;
|
|
380
|
+
});
|
|
381
|
+
const totalToolCalls = unique.length;
|
|
382
|
+
const mcpToolCalls = unique.filter((e) => e.is_mcp_tool === 1).length;
|
|
383
|
+
const directToolCalls = totalToolCalls - mcpToolCalls;
|
|
384
|
+
const subagentCount = unique.filter((e) => e.tool_type === 'subagent').length;
|
|
385
|
+
const timestamps = unique.map((e) => e.timestamp).filter(Boolean);
|
|
386
|
+
const durationMs = timestamps.length >= 2
|
|
387
|
+
? Math.max(...timestamps) - Math.min(...timestamps)
|
|
388
|
+
: 0;
|
|
389
|
+
this.db.prepare(`
|
|
390
|
+
UPDATE sessions
|
|
391
|
+
SET finished_at = ?, duration_ms = ?, total_tool_calls = ?,
|
|
392
|
+
mcp_tool_calls = ?, direct_tool_calls = ?, subagent_count = ?,
|
|
393
|
+
status = 'completed'
|
|
394
|
+
WHERE id = ?
|
|
395
|
+
`).run(finishedAt ?? Date.now(), durationMs, totalToolCalls, mcpToolCalls, directToolCalls, subagentCount, id);
|
|
396
|
+
}
|
|
397
|
+
listToolCallEventsBySession(sessionId) {
|
|
398
|
+
const rows = this.db.prepare('SELECT * FROM tool_call_events WHERE session_id = ? ORDER BY timestamp ASC').all(sessionId);
|
|
399
|
+
return rows.map(row => ({
|
|
400
|
+
eventId: row.event_id,
|
|
401
|
+
executionId: row.execution_id,
|
|
402
|
+
sessionId: row.session_id,
|
|
403
|
+
timestamp: row.timestamp,
|
|
404
|
+
toolName: row.tool_name,
|
|
405
|
+
toolType: row.tool_type,
|
|
406
|
+
isMcpTool: row.is_mcp_tool === 1,
|
|
407
|
+
mcpToolName: row.mcp_tool_name,
|
|
408
|
+
mcpEnforced: row.mcp_enforced === 1,
|
|
409
|
+
mcpFallback: row.mcp_fallback === 1,
|
|
410
|
+
kind: row.kind,
|
|
411
|
+
input: row.input ? JSON.parse(row.input) : null,
|
|
412
|
+
output: row.output ? JSON.parse(row.output) : null,
|
|
413
|
+
duration: row.duration,
|
|
414
|
+
parentToolCallId: row.parent_tool_call_id,
|
|
415
|
+
subagentId: row.subagent_id,
|
|
416
|
+
error: row.error,
|
|
417
|
+
blocked: row.blocked === 1,
|
|
418
|
+
blockReason: row.block_reason,
|
|
419
|
+
}));
|
|
420
|
+
}
|
|
421
|
+
listToolCallEventsBySessions(sessionIds) {
|
|
422
|
+
if (sessionIds.length === 0)
|
|
423
|
+
return {};
|
|
424
|
+
const placeholders = sessionIds.map(() => '?').join(',');
|
|
425
|
+
const rows = this.db.prepare(`SELECT * FROM tool_call_events WHERE session_id IN (${placeholders}) ORDER BY timestamp ASC`).all(...sessionIds);
|
|
426
|
+
const grouped = {};
|
|
427
|
+
for (const row of rows) {
|
|
428
|
+
const sid = row.session_id;
|
|
429
|
+
if (!grouped[sid])
|
|
430
|
+
grouped[sid] = [];
|
|
431
|
+
grouped[sid].push({
|
|
432
|
+
eventId: row.event_id,
|
|
433
|
+
executionId: row.execution_id,
|
|
434
|
+
sessionId: row.session_id,
|
|
435
|
+
timestamp: row.timestamp,
|
|
436
|
+
toolName: row.tool_name,
|
|
437
|
+
toolType: row.tool_type,
|
|
438
|
+
isMcpTool: row.is_mcp_tool === 1,
|
|
439
|
+
mcpToolName: row.mcp_tool_name,
|
|
440
|
+
mcpEnforced: row.mcp_enforced === 1,
|
|
441
|
+
mcpFallback: row.mcp_fallback === 1,
|
|
442
|
+
kind: row.kind,
|
|
443
|
+
input: row.input ? JSON.parse(row.input) : null,
|
|
444
|
+
output: row.output ? JSON.parse(row.output) : null,
|
|
445
|
+
duration: row.duration,
|
|
446
|
+
parentToolCallId: row.parent_tool_call_id,
|
|
447
|
+
subagentId: row.subagent_id,
|
|
448
|
+
error: row.error,
|
|
449
|
+
blocked: row.blocked === 1,
|
|
450
|
+
blockReason: row.block_reason,
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
return grouped;
|
|
454
|
+
}
|
|
237
455
|
// ---- Skill Executions ----
|
|
238
456
|
insertSkillExecution(params) {
|
|
239
457
|
this.db.prepare(`
|
|
240
458
|
INSERT INTO skill_executions
|
|
241
|
-
(execution_id, skill_name, started_at, status, required_mcp_tools, available_mcp_tools)
|
|
242
|
-
VALUES (?, ?, ?, ?, ?, ?)
|
|
243
|
-
`).run(params.executionId, params.skillName, params.startedAt, params.status, params.requiredMcpTools ? JSON.stringify(params.requiredMcpTools) : null, params.availableMcpTools ? JSON.stringify(params.availableMcpTools) : null);
|
|
459
|
+
(execution_id, session_id, skill_name, started_at, status, required_mcp_tools, available_mcp_tools)
|
|
460
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
461
|
+
`).run(params.executionId, params.sessionId ?? null, params.skillName, params.startedAt, params.status, params.requiredMcpTools ? JSON.stringify(params.requiredMcpTools) : null, params.availableMcpTools ? JSON.stringify(params.availableMcpTools) : null);
|
|
244
462
|
}
|
|
245
463
|
getSkillExecution(executionId) {
|
|
246
464
|
const row = this.db.prepare('SELECT * FROM skill_executions WHERE execution_id = ?').get(executionId);
|
|
@@ -248,6 +466,7 @@ class DevFlowDatabase {
|
|
|
248
466
|
return null;
|
|
249
467
|
return {
|
|
250
468
|
executionId: row.execution_id,
|
|
469
|
+
sessionId: row.session_id,
|
|
251
470
|
skillName: row.skill_name,
|
|
252
471
|
startedAt: row.started_at,
|
|
253
472
|
finishedAt: row.finished_at,
|
|
@@ -258,7 +477,6 @@ class DevFlowDatabase {
|
|
|
258
477
|
mcpToolCalls: row.mcp_tool_calls,
|
|
259
478
|
directToolCalls: row.direct_tool_calls,
|
|
260
479
|
subagentCount: row.subagent_count,
|
|
261
|
-
totalTokens: row.total_tokens,
|
|
262
480
|
totalDuration: row.total_duration,
|
|
263
481
|
mcpComplianceRate: row.mcp_compliance_rate,
|
|
264
482
|
missedMcpTools: row.missed_mcp_tools ? JSON.parse(row.missed_mcp_tools) : null,
|
|
@@ -280,10 +498,6 @@ class DevFlowDatabase {
|
|
|
280
498
|
sets.push('total_duration = ?');
|
|
281
499
|
values.push(updates.totalDuration);
|
|
282
500
|
}
|
|
283
|
-
if (updates.totalTokens !== undefined) {
|
|
284
|
-
sets.push('total_tokens = ?');
|
|
285
|
-
values.push(updates.totalTokens);
|
|
286
|
-
}
|
|
287
501
|
if (updates.mcpComplianceRate !== undefined) {
|
|
288
502
|
sets.push('mcp_compliance_rate = ?');
|
|
289
503
|
values.push(updates.mcpComplianceRate);
|
|
@@ -314,13 +528,6 @@ class DevFlowDatabase {
|
|
|
314
528
|
}
|
|
315
529
|
}
|
|
316
530
|
listSkillExecutions(limit, skillName) {
|
|
317
|
-
// Auto-timeout stale running executions (> 30 minutes)
|
|
318
|
-
const timeoutThreshold = Date.now() - 30 * 60 * 1000;
|
|
319
|
-
this.db.prepare(`
|
|
320
|
-
UPDATE skill_executions
|
|
321
|
-
SET status = 'failed', finished_at = ?
|
|
322
|
-
WHERE status = 'running' AND started_at < ?
|
|
323
|
-
`).run(Date.now(), timeoutThreshold);
|
|
324
531
|
let query = 'SELECT * FROM skill_executions';
|
|
325
532
|
const params = [];
|
|
326
533
|
if (skillName) {
|
|
@@ -332,6 +539,7 @@ class DevFlowDatabase {
|
|
|
332
539
|
const rows = this.db.prepare(query).all(...params);
|
|
333
540
|
return rows.map(row => ({
|
|
334
541
|
executionId: row.execution_id,
|
|
542
|
+
sessionId: row.session_id,
|
|
335
543
|
skillName: row.skill_name,
|
|
336
544
|
startedAt: row.started_at,
|
|
337
545
|
finishedAt: row.finished_at,
|
|
@@ -340,7 +548,6 @@ class DevFlowDatabase {
|
|
|
340
548
|
mcpToolCalls: row.mcp_tool_calls,
|
|
341
549
|
directToolCalls: row.direct_tool_calls,
|
|
342
550
|
subagentCount: row.subagent_count,
|
|
343
|
-
totalTokens: row.total_tokens,
|
|
344
551
|
totalDuration: row.total_duration,
|
|
345
552
|
mcpComplianceRate: row.mcp_compliance_rate,
|
|
346
553
|
}));
|
|
@@ -349,11 +556,11 @@ class DevFlowDatabase {
|
|
|
349
556
|
insertToolCallEvent(params) {
|
|
350
557
|
this.db.prepare(`
|
|
351
558
|
INSERT INTO tool_call_events
|
|
352
|
-
(event_id, execution_id, timestamp, tool_name, tool_type, is_mcp_tool,
|
|
353
|
-
mcp_tool_name, mcp_enforced, mcp_fallback, input, output,
|
|
354
|
-
parent_tool_call_id, subagent_id, error, blocked, block_reason)
|
|
355
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
356
|
-
`).run(params.eventId, params.executionId, params.timestamp, params.toolName, params.toolType, params.isMcpTool ? 1 : 0, params.mcpToolName || null, params.mcpEnforced ? 1 : 0, params.mcpFallback ? 1 : 0, JSON.stringify(params.input) ?? null, params.output ? JSON.stringify(params.output) : null, params.
|
|
559
|
+
(event_id, execution_id, session_id, timestamp, tool_name, tool_type, is_mcp_tool,
|
|
560
|
+
mcp_tool_name, mcp_enforced, mcp_fallback, kind, input, output, duration,
|
|
561
|
+
parent_tool_call_id, subagent_id, error, blocked, block_reason, workflow_run_id)
|
|
562
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
563
|
+
`).run(params.eventId, params.executionId || null, params.sessionId ?? null, params.timestamp, params.toolName, params.toolType, params.isMcpTool ? 1 : 0, params.mcpToolName || null, params.mcpEnforced ? 1 : 0, params.mcpFallback ? 1 : 0, params.kind ?? 'tool_use', JSON.stringify(params.input) ?? null, params.output ? JSON.stringify(params.output) : null, params.duration, params.parentToolCallId || null, params.subagentId || null, params.error || null, params.blocked ? 1 : 0, params.blockReason || null, params.workflowRunId || null);
|
|
357
564
|
// Update execution summary counters
|
|
358
565
|
this.db.prepare(`
|
|
359
566
|
UPDATE skill_executions
|
|
@@ -369,6 +576,7 @@ class DevFlowDatabase {
|
|
|
369
576
|
return rows.map(row => ({
|
|
370
577
|
eventId: row.event_id,
|
|
371
578
|
executionId: row.execution_id,
|
|
579
|
+
sessionId: row.session_id,
|
|
372
580
|
timestamp: row.timestamp,
|
|
373
581
|
toolName: row.tool_name,
|
|
374
582
|
toolType: row.tool_type,
|
|
@@ -376,9 +584,9 @@ class DevFlowDatabase {
|
|
|
376
584
|
mcpToolName: row.mcp_tool_name,
|
|
377
585
|
mcpEnforced: row.mcp_enforced === 1,
|
|
378
586
|
mcpFallback: row.mcp_fallback === 1,
|
|
587
|
+
kind: row.kind,
|
|
379
588
|
input: row.input ? JSON.parse(row.input) : null,
|
|
380
589
|
output: row.output ? JSON.parse(row.output) : null,
|
|
381
|
-
tokensUsed: row.tokens_used,
|
|
382
590
|
duration: row.duration,
|
|
383
591
|
parentToolCallId: row.parent_tool_call_id,
|
|
384
592
|
subagentId: row.subagent_id,
|
|
@@ -398,6 +606,10 @@ class DevFlowDatabase {
|
|
|
398
606
|
sets.push('error = ?');
|
|
399
607
|
values.push(updates.error);
|
|
400
608
|
}
|
|
609
|
+
if (updates.duration !== undefined) {
|
|
610
|
+
sets.push('duration = ?');
|
|
611
|
+
values.push(updates.duration);
|
|
612
|
+
}
|
|
401
613
|
if (sets.length > 0) {
|
|
402
614
|
values.push(eventId);
|
|
403
615
|
this.db.prepare(`UPDATE tool_call_events SET ${sets.join(', ')} WHERE event_id = ?`).run(...values);
|
|
@@ -441,6 +653,117 @@ class DevFlowDatabase {
|
|
|
441
653
|
const missedTools = missedRows.map((row) => row.mcp_tool_name);
|
|
442
654
|
return { overall, bySkill, missedTools };
|
|
443
655
|
}
|
|
656
|
+
cleanupStaleRecords() {
|
|
657
|
+
const cutoff = Date.now() - 30 * 60 * 1000;
|
|
658
|
+
try {
|
|
659
|
+
this.db.prepare(`UPDATE skill_executions SET status = 'failed', finished_at = ? WHERE status = 'running' AND started_at < ?`).run(Date.now(), cutoff);
|
|
660
|
+
this.db.prepare(`UPDATE sessions SET status = 'failed', finished_at = ? WHERE status = 'running' AND started_at < ?`).run(Date.now(), cutoff);
|
|
661
|
+
}
|
|
662
|
+
catch { /* best-effort */ }
|
|
663
|
+
}
|
|
664
|
+
insertToolMetric(metric) {
|
|
665
|
+
this.db.prepare(`
|
|
666
|
+
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)
|
|
667
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
668
|
+
`).run(metric.id, metric.toolCallEventId, metric.sessionId, metric.toolName, metric.query ?? null, metric.status ?? 'success', metric.resultCount ?? 0, metric.resultSizeBytes ?? 0, metric.latencyMs ?? 0, metric.engine ?? null, metric.accuracyQueryId ?? null, metric.metadata ?? null, metric.createdAt);
|
|
669
|
+
}
|
|
670
|
+
getToolMetrics(sessionId, toolName, limit = 100, offset = 0) {
|
|
671
|
+
let sql = 'SELECT * FROM tool_metrics WHERE 1=1';
|
|
672
|
+
const params = [];
|
|
673
|
+
if (sessionId) {
|
|
674
|
+
sql += ' AND session_id = ?';
|
|
675
|
+
params.push(sessionId);
|
|
676
|
+
}
|
|
677
|
+
if (toolName) {
|
|
678
|
+
sql += ' AND tool_name = ?';
|
|
679
|
+
params.push(toolName);
|
|
680
|
+
}
|
|
681
|
+
sql += ' ORDER BY created_at DESC LIMIT ? OFFSET ?';
|
|
682
|
+
params.push(limit, offset);
|
|
683
|
+
return this.db.prepare(sql).all(...params);
|
|
684
|
+
}
|
|
685
|
+
getToolMetricsSummary(days = 7) {
|
|
686
|
+
const cutoff = Date.now() - days * 24 * 60 * 60 * 1000;
|
|
687
|
+
const rows = this.db.prepare(`
|
|
688
|
+
SELECT tool_name, COUNT(*) as call_count,
|
|
689
|
+
SUM(CASE WHEN status = 'success' THEN 1 ELSE 0 END) as success_count,
|
|
690
|
+
SUM(CASE WHEN result_count = 0 THEN 1 ELSE 0 END) as empty_count,
|
|
691
|
+
AVG(result_count) as avg_results,
|
|
692
|
+
AVG(latency_ms) as avg_latency
|
|
693
|
+
FROM tool_metrics
|
|
694
|
+
WHERE created_at >= ?
|
|
695
|
+
GROUP BY tool_name
|
|
696
|
+
ORDER BY call_count DESC
|
|
697
|
+
`).all(cutoff);
|
|
698
|
+
return rows.map((r) => ({
|
|
699
|
+
toolName: r.tool_name,
|
|
700
|
+
callCount: r.call_count,
|
|
701
|
+
successRate: r.call_count > 0 ? Math.round((r.success_count / r.call_count) * 10000) / 100 : 0,
|
|
702
|
+
emptyRate: r.call_count > 0 ? Math.round((r.empty_count / r.call_count) * 10000) / 100 : 0,
|
|
703
|
+
avgResults: Math.round(r.avg_results * 10) / 10,
|
|
704
|
+
avgLatency: Math.round(r.avg_latency),
|
|
705
|
+
}));
|
|
706
|
+
}
|
|
707
|
+
// ---- Accuracy Queries ----
|
|
708
|
+
insertAccuracyQuery(q) {
|
|
709
|
+
this.db.prepare(`
|
|
710
|
+
INSERT INTO accuracy_queries (id, session_id, engine, query, top_k_results, selected_ids, created_at)
|
|
711
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
712
|
+
`).run(q.id, q.sessionId, q.engine, q.query, q.topKResults, q.selectedIds ?? null, q.createdAt);
|
|
713
|
+
}
|
|
714
|
+
getAccuracyQueries(options) {
|
|
715
|
+
let sql = 'SELECT * FROM accuracy_queries WHERE 1=1';
|
|
716
|
+
const params = [];
|
|
717
|
+
if (options.engine) {
|
|
718
|
+
sql += ' AND engine = ?';
|
|
719
|
+
params.push(options.engine);
|
|
720
|
+
}
|
|
721
|
+
if (options.status === 'annotated')
|
|
722
|
+
sql += ' AND relevance_feedback IS NOT NULL';
|
|
723
|
+
if (options.status === 'unannotated')
|
|
724
|
+
sql += ' AND relevance_feedback IS NULL';
|
|
725
|
+
sql += ' ORDER BY created_at DESC LIMIT ? OFFSET ?';
|
|
726
|
+
params.push(options.limit ?? 50, options.offset ?? 0);
|
|
727
|
+
const rows = this.db.prepare(sql).all(...params);
|
|
728
|
+
return rows.map((r) => ({
|
|
729
|
+
id: r.id, sessionId: r.session_id, engine: r.engine, query: r.query,
|
|
730
|
+
topKResults: r.top_k_results ? JSON.parse(r.top_k_results) : [],
|
|
731
|
+
selectedIds: r.selected_ids ? JSON.parse(r.selected_ids) : null,
|
|
732
|
+
relevanceFeedback: r.relevance_feedback,
|
|
733
|
+
annotatorNote: r.annotator_note,
|
|
734
|
+
annotatedAt: r.annotated_at,
|
|
735
|
+
createdAt: r.created_at,
|
|
736
|
+
}));
|
|
737
|
+
}
|
|
738
|
+
updateAccuracyFeedback(id, feedback) {
|
|
739
|
+
this.db.prepare('UPDATE accuracy_queries SET relevance_feedback = ?, annotator_note = ?, annotated_at = ? WHERE id = ?').run(feedback.relevance, feedback.note ?? null, Date.now(), id);
|
|
740
|
+
}
|
|
741
|
+
getAccuracyStats(engine, since) {
|
|
742
|
+
let where = 'WHERE 1=1';
|
|
743
|
+
const params = [];
|
|
744
|
+
if (engine) {
|
|
745
|
+
where += ' AND engine = ?';
|
|
746
|
+
params.push(engine);
|
|
747
|
+
}
|
|
748
|
+
if (since) {
|
|
749
|
+
where += ' AND created_at >= ?';
|
|
750
|
+
params.push(since);
|
|
751
|
+
}
|
|
752
|
+
const totalQueries = this.db.prepare(`SELECT COUNT(*) as c FROM accuracy_queries ${where}`).get(...params)?.c ?? 0;
|
|
753
|
+
const annotatedQueries = this.db.prepare(`SELECT COUNT(*) as c FROM accuracy_queries ${where} AND relevance_feedback IS NOT NULL`).get(...params)?.c ?? 0;
|
|
754
|
+
const hitCount = annotatedQueries > 0
|
|
755
|
+
? (this.db.prepare(`SELECT COUNT(*) as c FROM accuracy_queries ${where} AND relevance_feedback = 'hit'`).get(...params)?.c ?? 0)
|
|
756
|
+
: 0;
|
|
757
|
+
const hitRate = annotatedQueries > 0 ? hitCount / annotatedQueries : null;
|
|
758
|
+
return { totalQueries, annotatedQueries, hitRate };
|
|
759
|
+
}
|
|
760
|
+
// Convenience methods for raw SQL queries (used by AutoChecker)
|
|
761
|
+
all(sql, ...params) {
|
|
762
|
+
return this.db.prepare(sql).all(...params);
|
|
763
|
+
}
|
|
764
|
+
get(sql, ...params) {
|
|
765
|
+
return this.db.prepare(sql).get(...params);
|
|
766
|
+
}
|
|
444
767
|
close() {
|
|
445
768
|
this.db.close();
|
|
446
769
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@devflow-tools/database",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "DevFlow SQLite database package",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -13,5 +13,5 @@
|
|
|
13
13
|
"typescript": "^5.5.0",
|
|
14
14
|
"vitest": "^2.0.0"
|
|
15
15
|
},
|
|
16
|
-
"gitHead": "
|
|
16
|
+
"gitHead": "7872bccedbacd09cc92b983f30b3702db6878125"
|
|
17
17
|
}
|