@devflow-tools/database 0.15.0 → 0.16.2
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 +32 -0
- package/__tests__/database.failure-category.test.ts +44 -0
- package/__tests__/database.test.ts +30 -0
- package/__tests__/node-sqlite.test.ts +8 -0
- package/dist/database.d.ts +143 -2
- package/dist/database.js +658 -26
- package/dist/index.d.ts +2 -1
- package/dist/index.js +3 -1
- package/dist/node-sqlite.d.ts +1 -1
- package/dist/node-sqlite.js +8 -5
- package/package.json +2 -2
- package/src/database.ts +900 -57
- package/src/index.ts +15 -1
- package/src/node-sqlite.ts +9 -5
- package/tsconfig.tsbuildinfo +1 -1
package/dist/database.js
CHANGED
|
@@ -1,9 +1,22 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.DevFlowDatabase = void 0;
|
|
4
|
+
exports.getGlobalDevFlowDbPath = getGlobalDevFlowDbPath;
|
|
5
|
+
exports.openGlobalDevFlowDatabase = openGlobalDevFlowDatabase;
|
|
4
6
|
const node_sqlite_1 = require("./node-sqlite");
|
|
5
7
|
const path_1 = require("path");
|
|
6
8
|
const fs_1 = require("fs");
|
|
9
|
+
const os_1 = require("os");
|
|
10
|
+
function getGlobalDevFlowDbPath(home = (0, os_1.homedir)()) {
|
|
11
|
+
const stateDir = process.env.DEVFLOW_STATE_DIR ?? (0, path_1.join)(home, '.devflow', 'global');
|
|
12
|
+
return (0, path_1.join)(stateDir, 'devflow.db');
|
|
13
|
+
}
|
|
14
|
+
function openGlobalDevFlowDatabase(home = (0, os_1.homedir)(), options) {
|
|
15
|
+
return new DevFlowDatabase(home, {
|
|
16
|
+
dbPath: getGlobalDevFlowDbPath(home),
|
|
17
|
+
busyTimeoutMs: options?.busyTimeoutMs,
|
|
18
|
+
});
|
|
19
|
+
}
|
|
7
20
|
class DevFlowDatabase {
|
|
8
21
|
constructor(projectRoot, opts) {
|
|
9
22
|
let dbPath;
|
|
@@ -19,9 +32,9 @@ class DevFlowDatabase {
|
|
|
19
32
|
(0, fs_1.mkdirSync)(devflowDir, { recursive: true });
|
|
20
33
|
dbPath = (0, path_1.join)(devflowDir, 'devflow.db');
|
|
21
34
|
}
|
|
22
|
-
this.db = new node_sqlite_1.NodeSqliteDatabase(dbPath);
|
|
35
|
+
this.db = new node_sqlite_1.NodeSqliteDatabase(dbPath, opts?.busyTimeoutMs);
|
|
23
36
|
this.db.exec('PRAGMA journal_mode = WAL');
|
|
24
|
-
this.db.exec(
|
|
37
|
+
this.db.exec(`PRAGMA busy_timeout = ${Math.max(0, Math.floor(opts?.busyTimeoutMs ?? 5000))}`);
|
|
25
38
|
this.db.exec('PRAGMA foreign_keys = OFF');
|
|
26
39
|
this.initializeSchema();
|
|
27
40
|
}
|
|
@@ -123,7 +136,9 @@ class DevFlowDatabase {
|
|
|
123
136
|
subagent_id TEXT,
|
|
124
137
|
error TEXT,
|
|
125
138
|
blocked INTEGER DEFAULT 0 CHECK (blocked IN (0, 1)),
|
|
126
|
-
block_reason TEXT
|
|
139
|
+
block_reason TEXT,
|
|
140
|
+
failure_category TEXT,
|
|
141
|
+
tool_use_id TEXT
|
|
127
142
|
);
|
|
128
143
|
|
|
129
144
|
CREATE INDEX IF NOT EXISTS idx_executions_skill ON skill_executions(skill_name);
|
|
@@ -146,11 +161,99 @@ class DevFlowDatabase {
|
|
|
146
161
|
mcp_tool_calls INTEGER DEFAULT 0,
|
|
147
162
|
direct_tool_calls INTEGER DEFAULT 0,
|
|
148
163
|
subagent_count INTEGER DEFAULT 0,
|
|
149
|
-
status TEXT NOT NULL DEFAULT 'running' CHECK (status IN ('running', 'completed', 'failed'))
|
|
164
|
+
status TEXT NOT NULL DEFAULT 'running' CHECK (status IN ('running', 'completed', 'failed')),
|
|
165
|
+
metadata TEXT
|
|
150
166
|
);
|
|
151
167
|
|
|
152
168
|
CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_root);
|
|
153
169
|
CREATE INDEX IF NOT EXISTS idx_sessions_started ON sessions(started_at DESC);
|
|
170
|
+
|
|
171
|
+
CREATE TABLE IF NOT EXISTS feedback (
|
|
172
|
+
id TEXT PRIMARY KEY,
|
|
173
|
+
project_root TEXT NOT NULL,
|
|
174
|
+
query TEXT NOT NULL,
|
|
175
|
+
rating TEXT NOT NULL CHECK (rating IN ('hit', 'partial', 'miss')),
|
|
176
|
+
task_type TEXT,
|
|
177
|
+
context_mode TEXT,
|
|
178
|
+
run_id TEXT,
|
|
179
|
+
token_count INTEGER DEFAULT 0,
|
|
180
|
+
created_at INTEGER NOT NULL
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
CREATE INDEX IF NOT EXISTS idx_feedback_project ON feedback(project_root);
|
|
184
|
+
CREATE INDEX IF NOT EXISTS idx_feedback_created ON feedback(created_at DESC);
|
|
185
|
+
|
|
186
|
+
CREATE TABLE IF NOT EXISTS telemetry_failures (
|
|
187
|
+
id TEXT PRIMARY KEY,
|
|
188
|
+
operation TEXT NOT NULL,
|
|
189
|
+
payload TEXT,
|
|
190
|
+
error TEXT NOT NULL,
|
|
191
|
+
created_at INTEGER NOT NULL,
|
|
192
|
+
resolved_at INTEGER
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
CREATE INDEX IF NOT EXISTS idx_telemetry_failures_created ON telemetry_failures(created_at DESC);
|
|
196
|
+
CREATE INDEX IF NOT EXISTS idx_telemetry_failures_unresolved ON telemetry_failures(resolved_at) WHERE resolved_at IS NULL;
|
|
197
|
+
|
|
198
|
+
CREATE TABLE IF NOT EXISTS devflow_rules (
|
|
199
|
+
id TEXT PRIMARY KEY,
|
|
200
|
+
name TEXT NOT NULL,
|
|
201
|
+
level TEXT NOT NULL DEFAULT 'project',
|
|
202
|
+
gate INTEGER NOT NULL DEFAULT 1,
|
|
203
|
+
condition_json TEXT NOT NULL,
|
|
204
|
+
action TEXT NOT NULL DEFAULT 'warn',
|
|
205
|
+
message TEXT NOT NULL,
|
|
206
|
+
enabled INTEGER NOT NULL DEFAULT 1,
|
|
207
|
+
created_at INTEGER NOT NULL,
|
|
208
|
+
updated_at INTEGER NOT NULL
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
CREATE TABLE IF NOT EXISTS devflow_audit_chain (
|
|
212
|
+
seq INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
213
|
+
ts INTEGER NOT NULL,
|
|
214
|
+
tool TEXT NOT NULL,
|
|
215
|
+
args_hash TEXT NOT NULL,
|
|
216
|
+
decision TEXT NOT NULL,
|
|
217
|
+
rule_id TEXT,
|
|
218
|
+
session_id TEXT NOT NULL,
|
|
219
|
+
prev_hash TEXT NOT NULL,
|
|
220
|
+
signature TEXT NOT NULL
|
|
221
|
+
);
|
|
222
|
+
|
|
223
|
+
CREATE INDEX IF NOT EXISTS idx_devflow_audit_ts ON devflow_audit_chain(ts DESC);
|
|
224
|
+
|
|
225
|
+
CREATE TABLE IF NOT EXISTS devflow_hook_receipts (
|
|
226
|
+
project_root TEXT PRIMARY KEY,
|
|
227
|
+
last_mcp_call INTEGER,
|
|
228
|
+
bypass_count INTEGER NOT NULL DEFAULT 0,
|
|
229
|
+
updated_at INTEGER NOT NULL
|
|
230
|
+
);
|
|
231
|
+
|
|
232
|
+
CREATE TABLE IF NOT EXISTS devflow_context_receipts (
|
|
233
|
+
project_root TEXT NOT NULL,
|
|
234
|
+
session_id TEXT NOT NULL,
|
|
235
|
+
execution_id TEXT NOT NULL,
|
|
236
|
+
context_hash TEXT NOT NULL,
|
|
237
|
+
issued_at INTEGER NOT NULL,
|
|
238
|
+
expires_at INTEGER NOT NULL,
|
|
239
|
+
PRIMARY KEY (project_root, session_id, execution_id)
|
|
240
|
+
);
|
|
241
|
+
|
|
242
|
+
CREATE INDEX IF NOT EXISTS idx_context_receipts_expiry
|
|
243
|
+
ON devflow_context_receipts(expires_at);
|
|
244
|
+
|
|
245
|
+
CREATE TABLE IF NOT EXISTS devflow_memory_distill_checkpoints (
|
|
246
|
+
id TEXT PRIMARY KEY,
|
|
247
|
+
project_root TEXT NOT NULL,
|
|
248
|
+
session_id TEXT,
|
|
249
|
+
trigger TEXT NOT NULL CHECK(trigger IN ('pre_compact', 'session_end')),
|
|
250
|
+
pending_events INTEGER NOT NULL DEFAULT 0,
|
|
251
|
+
released_leases INTEGER NOT NULL DEFAULT 0,
|
|
252
|
+
created_at INTEGER NOT NULL
|
|
253
|
+
);
|
|
254
|
+
|
|
255
|
+
CREATE INDEX IF NOT EXISTS idx_distill_checkpoints_project
|
|
256
|
+
ON devflow_memory_distill_checkpoints(project_root, created_at DESC);
|
|
154
257
|
`);
|
|
155
258
|
// Migration: add session_id to skill_executions (SQLite compat — ignore if exists)
|
|
156
259
|
try {
|
|
@@ -177,6 +280,37 @@ class DevFlowDatabase {
|
|
|
177
280
|
this.db.exec('ALTER TABLE tool_call_events ADD COLUMN workflow_run_id TEXT');
|
|
178
281
|
}
|
|
179
282
|
catch { }
|
|
283
|
+
try {
|
|
284
|
+
this.db.exec('ALTER TABLE tool_call_events ADD COLUMN failure_category TEXT');
|
|
285
|
+
}
|
|
286
|
+
catch { }
|
|
287
|
+
try {
|
|
288
|
+
this.db.exec('ALTER TABLE tool_call_events ADD COLUMN tool_use_id TEXT');
|
|
289
|
+
}
|
|
290
|
+
catch { }
|
|
291
|
+
try {
|
|
292
|
+
this.db.exec('ALTER TABLE sessions ADD COLUMN metadata TEXT');
|
|
293
|
+
}
|
|
294
|
+
catch { }
|
|
295
|
+
try {
|
|
296
|
+
this.db.exec('ALTER TABLE skill_executions ADD COLUMN metadata TEXT');
|
|
297
|
+
}
|
|
298
|
+
catch { }
|
|
299
|
+
try {
|
|
300
|
+
this.db.exec(`CREATE UNIQUE INDEX IF NOT EXISTS idx_events_session_tool_use
|
|
301
|
+
ON tool_call_events(session_id, tool_use_id) WHERE tool_use_id IS NOT NULL`);
|
|
302
|
+
}
|
|
303
|
+
catch { }
|
|
304
|
+
try {
|
|
305
|
+
this.db.exec('ALTER TABLE devflow_rules ADD COLUMN gate INTEGER NOT NULL DEFAULT 1');
|
|
306
|
+
}
|
|
307
|
+
catch { }
|
|
308
|
+
try {
|
|
309
|
+
this.db.exec('ALTER TABLE devflow_rules ADD COLUMN updated_at INTEGER');
|
|
310
|
+
}
|
|
311
|
+
catch { }
|
|
312
|
+
this.db.exec('UPDATE devflow_rules SET updated_at = created_at WHERE updated_at IS NULL');
|
|
313
|
+
this.db.exec('CREATE INDEX IF NOT EXISTS idx_devflow_rules_gate ON devflow_rules(gate, enabled)');
|
|
180
314
|
// Migration: tool_metrics table for per-tool metrics collection
|
|
181
315
|
try {
|
|
182
316
|
this.db.exec(`
|
|
@@ -221,6 +355,28 @@ class DevFlowDatabase {
|
|
|
221
355
|
CREATE INDEX IF NOT EXISTS idx_accuracy_queries_engine ON accuracy_queries(engine);
|
|
222
356
|
CREATE INDEX IF NOT EXISTS idx_accuracy_queries_session ON accuracy_queries(session_id);
|
|
223
357
|
CREATE INDEX IF NOT EXISTS idx_accuracy_queries_feedback ON accuracy_queries(relevance_feedback);
|
|
358
|
+
`);
|
|
359
|
+
}
|
|
360
|
+
catch { }
|
|
361
|
+
// Canonical benchmark reports live in the global DevFlow database.
|
|
362
|
+
try {
|
|
363
|
+
this.db.exec(`
|
|
364
|
+
CREATE TABLE IF NOT EXISTS benchmark_reports (
|
|
365
|
+
run_id TEXT PRIMARY KEY,
|
|
366
|
+
suite_id TEXT NOT NULL,
|
|
367
|
+
suite_version TEXT NOT NULL,
|
|
368
|
+
project_root TEXT NOT NULL,
|
|
369
|
+
commit_sha TEXT,
|
|
370
|
+
task_count INTEGER NOT NULL,
|
|
371
|
+
status TEXT NOT NULL CHECK(status IN ('completed')),
|
|
372
|
+
report_json TEXT NOT NULL,
|
|
373
|
+
created_at INTEGER NOT NULL
|
|
374
|
+
);
|
|
375
|
+
|
|
376
|
+
CREATE INDEX IF NOT EXISTS idx_benchmark_reports_created
|
|
377
|
+
ON benchmark_reports(created_at DESC);
|
|
378
|
+
CREATE INDEX IF NOT EXISTS idx_benchmark_reports_suite
|
|
379
|
+
ON benchmark_reports(suite_id, suite_version, created_at DESC);
|
|
224
380
|
`);
|
|
225
381
|
}
|
|
226
382
|
catch { }
|
|
@@ -272,7 +428,7 @@ class DevFlowDatabase {
|
|
|
272
428
|
}
|
|
273
429
|
insertRun(run) {
|
|
274
430
|
this.db.prepare(`
|
|
275
|
-
INSERT INTO runs (id, source, tool, input, status, started_at, finished_at, token_used, metadata, created_at)
|
|
431
|
+
INSERT OR IGNORE INTO runs (id, source, tool, input, status, started_at, finished_at, token_used, metadata, created_at)
|
|
276
432
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
277
433
|
`).run(run.id, run.source, run.tool, JSON.stringify(run.input) ?? null, run.status, run.startedAt, run.finishedAt ?? null, run.tokenUsed ?? 0, JSON.stringify(run.metadata) ?? null, Date.now());
|
|
278
434
|
}
|
|
@@ -383,6 +539,22 @@ class DevFlowDatabase {
|
|
|
383
539
|
VALUES (?, ?, ?, ?, 'running')
|
|
384
540
|
`).run(params.id, params.projectRoot, params.label ?? null, params.startedAt);
|
|
385
541
|
}
|
|
542
|
+
ensureSession(params) {
|
|
543
|
+
if (!params.id.trim())
|
|
544
|
+
throw new Error('Canonical session ID is required');
|
|
545
|
+
const startedAt = params.startedAt ?? Date.now();
|
|
546
|
+
this.db.prepare(`
|
|
547
|
+
INSERT INTO sessions (id, project_root, label, started_at, status)
|
|
548
|
+
VALUES (?, ?, ?, ?, 'running')
|
|
549
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
550
|
+
project_root = CASE
|
|
551
|
+
WHEN sessions.project_root = '' OR sessions.project_root = 'unknown' THEN excluded.project_root
|
|
552
|
+
ELSE sessions.project_root
|
|
553
|
+
END,
|
|
554
|
+
label = COALESCE(sessions.label, excluded.label),
|
|
555
|
+
started_at = MIN(sessions.started_at, excluded.started_at)
|
|
556
|
+
`).run(params.id, params.projectRoot || 'unknown', params.label ?? null, startedAt);
|
|
557
|
+
}
|
|
386
558
|
getSession(id) {
|
|
387
559
|
const row = this.db.prepare('SELECT * FROM sessions WHERE id = ?').get(id);
|
|
388
560
|
if (!row)
|
|
@@ -399,6 +571,7 @@ class DevFlowDatabase {
|
|
|
399
571
|
directToolCalls: row.direct_tool_calls,
|
|
400
572
|
subagentCount: row.subagent_count,
|
|
401
573
|
status: row.status,
|
|
574
|
+
metadata: row.metadata ? JSON.parse(row.metadata) : null,
|
|
402
575
|
};
|
|
403
576
|
}
|
|
404
577
|
listSessions(limit, offset, projectRoot) {
|
|
@@ -423,6 +596,7 @@ class DevFlowDatabase {
|
|
|
423
596
|
directToolCalls: row.direct_tool_calls,
|
|
424
597
|
subagentCount: row.subagent_count,
|
|
425
598
|
status: row.status,
|
|
599
|
+
metadata: row.metadata ? JSON.parse(row.metadata) : null,
|
|
426
600
|
}));
|
|
427
601
|
}
|
|
428
602
|
closeSession(id, finishedAt) {
|
|
@@ -476,6 +650,8 @@ class DevFlowDatabase {
|
|
|
476
650
|
error: row.error,
|
|
477
651
|
blocked: row.blocked === 1,
|
|
478
652
|
blockReason: row.block_reason,
|
|
653
|
+
failureCategory: row.failure_category,
|
|
654
|
+
toolUseId: row.tool_use_id,
|
|
479
655
|
tokensUsed: row.tokens_used ?? 0,
|
|
480
656
|
}));
|
|
481
657
|
}
|
|
@@ -509,6 +685,8 @@ class DevFlowDatabase {
|
|
|
509
685
|
error: row.error,
|
|
510
686
|
blocked: row.blocked === 1,
|
|
511
687
|
blockReason: row.block_reason,
|
|
688
|
+
failureCategory: row.failure_category,
|
|
689
|
+
toolUseId: row.tool_use_id,
|
|
512
690
|
tokensUsed: row.tokens_used ?? 0,
|
|
513
691
|
});
|
|
514
692
|
}
|
|
@@ -517,7 +695,7 @@ class DevFlowDatabase {
|
|
|
517
695
|
// ---- Skill Executions ----
|
|
518
696
|
insertSkillExecution(params) {
|
|
519
697
|
this.db.prepare(`
|
|
520
|
-
INSERT INTO skill_executions
|
|
698
|
+
INSERT OR IGNORE INTO skill_executions
|
|
521
699
|
(execution_id, session_id, skill_name, started_at, status, required_mcp_tools, available_mcp_tools)
|
|
522
700
|
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
523
701
|
`).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);
|
|
@@ -543,6 +721,7 @@ class DevFlowDatabase {
|
|
|
543
721
|
mcpComplianceRate: row.mcp_compliance_rate,
|
|
544
722
|
missedMcpTools: row.missed_mcp_tools ? JSON.parse(row.missed_mcp_tools) : null,
|
|
545
723
|
createdAt: row.created_at,
|
|
724
|
+
metadata: row.metadata ? JSON.parse(row.metadata) : null,
|
|
546
725
|
};
|
|
547
726
|
}
|
|
548
727
|
updateSkillExecution(executionId, updates) {
|
|
@@ -584,6 +763,10 @@ class DevFlowDatabase {
|
|
|
584
763
|
sets.push('subagent_count = ?');
|
|
585
764
|
values.push(updates.subagentCount);
|
|
586
765
|
}
|
|
766
|
+
if (updates.metadata !== undefined) {
|
|
767
|
+
sets.push('metadata = ?');
|
|
768
|
+
values.push(JSON.stringify(updates.metadata));
|
|
769
|
+
}
|
|
587
770
|
if (sets.length > 0) {
|
|
588
771
|
values.push(executionId);
|
|
589
772
|
this.db.prepare(`UPDATE skill_executions SET ${sets.join(', ')} WHERE execution_id = ?`).run(...values);
|
|
@@ -612,26 +795,47 @@ class DevFlowDatabase {
|
|
|
612
795
|
subagentCount: row.subagent_count,
|
|
613
796
|
totalDuration: row.total_duration,
|
|
614
797
|
mcpComplianceRate: row.mcp_compliance_rate,
|
|
798
|
+
metadata: row.metadata ? JSON.parse(row.metadata) : null,
|
|
615
799
|
}));
|
|
616
800
|
}
|
|
617
801
|
// ---- Tool Call Events ----
|
|
618
802
|
insertToolCallEvent(params) {
|
|
619
|
-
this.db.
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
803
|
+
return this.db.transaction(() => {
|
|
804
|
+
const result = this.db.prepare(`
|
|
805
|
+
INSERT OR IGNORE INTO tool_call_events
|
|
806
|
+
(event_id, execution_id, session_id, timestamp, tool_name, tool_type, is_mcp_tool,
|
|
807
|
+
mcp_tool_name, mcp_enforced, mcp_fallback, kind, input, output, tokens_used, duration,
|
|
808
|
+
parent_tool_call_id, subagent_id, error, blocked, block_reason, workflow_run_id,
|
|
809
|
+
failure_category, tool_use_id)
|
|
810
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
811
|
+
`).run(params.eventId, params.executionId || '', 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 !== undefined ? JSON.stringify(params.output) : null, params.tokensUsed ?? 0, params.duration, params.parentToolCallId || null, params.subagentId || null, params.error || null, params.blocked ? 1 : 0, params.blockReason || null, params.workflowRunId || null, params.failureCategory || null, params.toolUseId || null);
|
|
812
|
+
if (result.changes === 0)
|
|
813
|
+
return false;
|
|
814
|
+
this.db.prepare(`
|
|
815
|
+
UPDATE skill_executions
|
|
816
|
+
SET total_tool_calls = total_tool_calls + 1,
|
|
817
|
+
mcp_tool_calls = mcp_tool_calls + ?,
|
|
818
|
+
direct_tool_calls = direct_tool_calls + ?,
|
|
819
|
+
subagent_count = subagent_count + ?,
|
|
820
|
+
total_duration = MAX(total_duration, ? - started_at),
|
|
821
|
+
mcp_compliance_rate = ROUND(
|
|
822
|
+
100.0 * (mcp_tool_calls + ?) / NULLIF(total_tool_calls + 1, 0), 2
|
|
823
|
+
)
|
|
824
|
+
WHERE execution_id = ?
|
|
825
|
+
`).run(params.isMcpTool ? 1 : 0, params.isMcpTool ? 0 : 1, params.toolType === 'subagent' ? 1 : 0, params.timestamp, params.isMcpTool ? 1 : 0, params.executionId);
|
|
826
|
+
if (params.sessionId) {
|
|
827
|
+
this.db.prepare(`
|
|
828
|
+
UPDATE sessions
|
|
829
|
+
SET total_tool_calls = total_tool_calls + 1,
|
|
830
|
+
mcp_tool_calls = mcp_tool_calls + ?,
|
|
831
|
+
direct_tool_calls = direct_tool_calls + ?,
|
|
832
|
+
subagent_count = subagent_count + ?,
|
|
833
|
+
duration_ms = MAX(duration_ms, ? - started_at)
|
|
834
|
+
WHERE id = ?
|
|
835
|
+
`).run(params.isMcpTool ? 1 : 0, params.isMcpTool ? 0 : 1, params.toolType === 'subagent' ? 1 : 0, params.timestamp, params.sessionId);
|
|
836
|
+
}
|
|
837
|
+
return true;
|
|
838
|
+
});
|
|
635
839
|
}
|
|
636
840
|
listToolCallEvents(executionId) {
|
|
637
841
|
const rows = this.db.prepare('SELECT * FROM tool_call_events WHERE execution_id = ? ORDER BY timestamp ASC').all(executionId);
|
|
@@ -655,6 +859,8 @@ class DevFlowDatabase {
|
|
|
655
859
|
error: row.error,
|
|
656
860
|
blocked: row.blocked === 1,
|
|
657
861
|
blockReason: row.block_reason,
|
|
862
|
+
failureCategory: row.failure_category,
|
|
863
|
+
toolUseId: row.tool_use_id,
|
|
658
864
|
}));
|
|
659
865
|
}
|
|
660
866
|
updateToolCallEvent(eventId, updates) {
|
|
@@ -674,8 +880,41 @@ class DevFlowDatabase {
|
|
|
674
880
|
}
|
|
675
881
|
if (sets.length > 0) {
|
|
676
882
|
values.push(eventId);
|
|
677
|
-
this.db.prepare(`UPDATE tool_call_events SET ${sets.join(', ')} WHERE event_id = ?`).run(...values);
|
|
883
|
+
const result = this.db.prepare(`UPDATE tool_call_events SET ${sets.join(', ')} WHERE event_id = ?`).run(...values);
|
|
884
|
+
return result.changes > 0;
|
|
678
885
|
}
|
|
886
|
+
return false;
|
|
887
|
+
}
|
|
888
|
+
markToolCallBlocked(eventId, reason) {
|
|
889
|
+
return this.db.prepare(`
|
|
890
|
+
UPDATE tool_call_events
|
|
891
|
+
SET blocked = 1, block_reason = ?, error = COALESCE(error, ?)
|
|
892
|
+
WHERE event_id = ?
|
|
893
|
+
`).run(reason, reason, eventId).changes > 0;
|
|
894
|
+
}
|
|
895
|
+
reconcileSkillExecution(executionId, status, finishedAt = Date.now(), metadata) {
|
|
896
|
+
const row = this.db.prepare(`
|
|
897
|
+
SELECT COUNT(*) AS total,
|
|
898
|
+
SUM(CASE WHEN is_mcp_tool = 1 THEN 1 ELSE 0 END) AS mcp,
|
|
899
|
+
SUM(CASE WHEN is_mcp_tool = 0 THEN 1 ELSE 0 END) AS direct,
|
|
900
|
+
SUM(CASE WHEN tool_type = 'subagent' THEN 1 ELSE 0 END) AS subagents,
|
|
901
|
+
MIN(timestamp) AS first_at,
|
|
902
|
+
MAX(timestamp) AS last_at
|
|
903
|
+
FROM tool_call_events WHERE execution_id = ?
|
|
904
|
+
`).get(executionId);
|
|
905
|
+
const total = Number(row?.total ?? 0);
|
|
906
|
+
const mcp = Number(row?.mcp ?? 0);
|
|
907
|
+
this.updateSkillExecution(executionId, {
|
|
908
|
+
status,
|
|
909
|
+
finishedAt,
|
|
910
|
+
totalToolCalls: total,
|
|
911
|
+
mcpToolCalls: mcp,
|
|
912
|
+
directToolCalls: Number(row?.direct ?? 0),
|
|
913
|
+
subagentCount: Number(row?.subagents ?? 0),
|
|
914
|
+
totalDuration: row?.first_at != null && row?.last_at != null ? row.last_at - row.first_at : 0,
|
|
915
|
+
mcpComplianceRate: total > 0 ? Math.round((mcp / total) * 10000) / 100 : 0,
|
|
916
|
+
metadata,
|
|
917
|
+
});
|
|
679
918
|
}
|
|
680
919
|
// ---- MCP Compliance ----
|
|
681
920
|
getMcpCompliance() {
|
|
@@ -718,14 +957,16 @@ class DevFlowDatabase {
|
|
|
718
957
|
cleanupStaleRecords() {
|
|
719
958
|
const cutoff = Date.now() - 30 * 60 * 1000;
|
|
720
959
|
try {
|
|
721
|
-
this.db.prepare(`UPDATE skill_executions SET status = 'failed', finished_at =
|
|
722
|
-
|
|
960
|
+
this.db.prepare(`UPDATE skill_executions SET status = 'failed', finished_at = ?, metadata = ?
|
|
961
|
+
WHERE status = 'running' AND started_at < ?`).run(Date.now(), JSON.stringify({ closureReason: 'abandoned' }), cutoff);
|
|
962
|
+
this.db.prepare(`UPDATE sessions SET status = 'failed', finished_at = ?, metadata = ?
|
|
963
|
+
WHERE status = 'running' AND started_at < ?`).run(Date.now(), JSON.stringify({ closureReason: 'abandoned' }), cutoff);
|
|
723
964
|
}
|
|
724
965
|
catch { /* best-effort */ }
|
|
725
966
|
}
|
|
726
967
|
insertToolMetric(metric) {
|
|
727
968
|
this.db.prepare(`
|
|
728
|
-
INSERT OR
|
|
969
|
+
INSERT OR IGNORE 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)
|
|
729
970
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
730
971
|
`).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);
|
|
731
972
|
}
|
|
@@ -766,6 +1007,111 @@ class DevFlowDatabase {
|
|
|
766
1007
|
avgLatency: Math.round(r.avg_latency),
|
|
767
1008
|
}));
|
|
768
1009
|
}
|
|
1010
|
+
aggregatePendingToolMetrics(limit = 1000) {
|
|
1011
|
+
const rows = this.db.prepare(`
|
|
1012
|
+
SELECT e.*
|
|
1013
|
+
FROM tool_call_events e
|
|
1014
|
+
LEFT JOIN tool_metrics m ON m.tool_call_event_id = e.event_id
|
|
1015
|
+
WHERE m.id IS NULL AND e.output IS NOT NULL
|
|
1016
|
+
ORDER BY e.timestamp ASC
|
|
1017
|
+
LIMIT ?
|
|
1018
|
+
`).all(limit);
|
|
1019
|
+
let inserted = 0;
|
|
1020
|
+
this.db.transaction(() => {
|
|
1021
|
+
for (const row of rows) {
|
|
1022
|
+
const input = parseJson(row.input);
|
|
1023
|
+
const output = parseJson(row.output);
|
|
1024
|
+
const toolName = row.mcp_tool_name || row.tool_name;
|
|
1025
|
+
const resultCount = countTelemetryResults(output);
|
|
1026
|
+
const resultSizeBytes = Buffer.byteLength(row.output ?? '', 'utf8');
|
|
1027
|
+
const status = row.error || row.blocked
|
|
1028
|
+
? 'error'
|
|
1029
|
+
: resultCount === 0 ? 'empty' : 'success';
|
|
1030
|
+
const result = this.db.prepare(`
|
|
1031
|
+
INSERT OR IGNORE INTO tool_metrics
|
|
1032
|
+
(id, tool_call_event_id, session_id, tool_name, query, status, result_count,
|
|
1033
|
+
result_size_bytes, latency_ms, engine, metadata, created_at)
|
|
1034
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1035
|
+
`).run(`metric:${row.event_id}`, row.event_id, row.session_id || '', toolName, deriveQuery(input), status, resultCount, resultSizeBytes, row.duration ?? 0, deriveEngine(toolName), JSON.stringify({ executionId: row.execution_id, kind: row.kind }), row.timestamp);
|
|
1036
|
+
inserted += result.changes;
|
|
1037
|
+
}
|
|
1038
|
+
});
|
|
1039
|
+
return inserted;
|
|
1040
|
+
}
|
|
1041
|
+
// ---- Context Feedback ----
|
|
1042
|
+
insertFeedback(feedback) {
|
|
1043
|
+
this.db.prepare(`
|
|
1044
|
+
INSERT OR IGNORE INTO feedback
|
|
1045
|
+
(id, project_root, query, rating, task_type, context_mode, run_id, token_count, created_at)
|
|
1046
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1047
|
+
`).run(feedback.id, feedback.projectRoot, feedback.query, feedback.rating, feedback.taskType ?? null, feedback.contextMode ?? null, feedback.runId ?? null, feedback.tokenCount ?? 0, feedback.createdAt);
|
|
1048
|
+
}
|
|
1049
|
+
listFeedback(projectRoot, limit = 100) {
|
|
1050
|
+
const rows = projectRoot
|
|
1051
|
+
? this.db.prepare('SELECT * FROM feedback WHERE project_root = ? ORDER BY created_at DESC LIMIT ?').all(projectRoot, limit)
|
|
1052
|
+
: this.db.prepare('SELECT * FROM feedback ORDER BY created_at DESC LIMIT ?').all(limit);
|
|
1053
|
+
return rows.map(row => ({
|
|
1054
|
+
id: row.id,
|
|
1055
|
+
projectRoot: row.project_root,
|
|
1056
|
+
query: row.query,
|
|
1057
|
+
rating: row.rating,
|
|
1058
|
+
taskType: row.task_type ?? undefined,
|
|
1059
|
+
contextMode: row.context_mode ?? undefined,
|
|
1060
|
+
runId: row.run_id ?? undefined,
|
|
1061
|
+
tokenCount: row.token_count ?? 0,
|
|
1062
|
+
createdAt: row.created_at,
|
|
1063
|
+
}));
|
|
1064
|
+
}
|
|
1065
|
+
getFeedbackSummary(projectRoot) {
|
|
1066
|
+
const row = (projectRoot
|
|
1067
|
+
? this.db.prepare(`SELECT COUNT(*) total, SUM(rating = 'hit') hits, SUM(rating = 'partial') partials, SUM(rating = 'miss') misses FROM feedback WHERE project_root = ?`).get(projectRoot)
|
|
1068
|
+
: this.db.prepare(`SELECT COUNT(*) total, SUM(rating = 'hit') hits, SUM(rating = 'partial') partials, SUM(rating = 'miss') misses FROM feedback`).get());
|
|
1069
|
+
const total = Number(row?.total ?? 0);
|
|
1070
|
+
const hits = Number(row?.hits ?? 0);
|
|
1071
|
+
const partials = Number(row?.partials ?? 0);
|
|
1072
|
+
const misses = Number(row?.misses ?? 0);
|
|
1073
|
+
return { total, hits, partials, misses, hitRate: total > 0 ? hits / total : 0 };
|
|
1074
|
+
}
|
|
1075
|
+
// ---- Telemetry Failures ----
|
|
1076
|
+
insertTelemetryFailure(failure) {
|
|
1077
|
+
this.db.prepare(`
|
|
1078
|
+
INSERT OR IGNORE INTO telemetry_failures
|
|
1079
|
+
(id, operation, payload, error, created_at, resolved_at)
|
|
1080
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
1081
|
+
`).run(failure.id, failure.operation, JSON.stringify(failure.payload) ?? null, failure.error, failure.createdAt, failure.resolvedAt ?? null);
|
|
1082
|
+
}
|
|
1083
|
+
resolveTelemetryFailure(id, resolvedAt = Date.now()) {
|
|
1084
|
+
this.db.prepare('UPDATE telemetry_failures SET resolved_at = ? WHERE id = ?').run(resolvedAt, id);
|
|
1085
|
+
}
|
|
1086
|
+
listTelemetryFailures(options) {
|
|
1087
|
+
const where = options?.unresolvedOnly ? 'WHERE resolved_at IS NULL' : '';
|
|
1088
|
+
const rows = this.db.prepare(`SELECT * FROM telemetry_failures ${where} ORDER BY created_at DESC LIMIT ?`)
|
|
1089
|
+
.all(options?.limit ?? 100);
|
|
1090
|
+
return rows.map(row => ({
|
|
1091
|
+
id: row.id,
|
|
1092
|
+
operation: row.operation,
|
|
1093
|
+
payload: parseJson(row.payload),
|
|
1094
|
+
error: row.error,
|
|
1095
|
+
createdAt: row.created_at,
|
|
1096
|
+
resolvedAt: row.resolved_at ?? undefined,
|
|
1097
|
+
}));
|
|
1098
|
+
}
|
|
1099
|
+
countTelemetryFailures(unresolvedOnly = false) {
|
|
1100
|
+
const row = this.db.prepare(`SELECT COUNT(*) count FROM telemetry_failures ${unresolvedOnly ? 'WHERE resolved_at IS NULL' : ''}`).get();
|
|
1101
|
+
return Number(row?.count ?? 0);
|
|
1102
|
+
}
|
|
1103
|
+
trimTelemetryFailures(maxRows = 500) {
|
|
1104
|
+
const limit = Math.max(1, Math.floor(maxRows));
|
|
1105
|
+
const result = this.db.prepare(`
|
|
1106
|
+
DELETE FROM telemetry_failures
|
|
1107
|
+
WHERE id IN (
|
|
1108
|
+
SELECT id FROM telemetry_failures
|
|
1109
|
+
ORDER BY created_at DESC, id DESC
|
|
1110
|
+
LIMIT -1 OFFSET ?
|
|
1111
|
+
)
|
|
1112
|
+
`).run(limit);
|
|
1113
|
+
return Number(result.changes);
|
|
1114
|
+
}
|
|
769
1115
|
// ---- Accuracy Queries ----
|
|
770
1116
|
insertAccuracyQuery(q) {
|
|
771
1117
|
this.db.prepare(`
|
|
@@ -830,6 +1176,241 @@ class DevFlowDatabase {
|
|
|
830
1176
|
}
|
|
831
1177
|
return statsByEngine;
|
|
832
1178
|
}
|
|
1179
|
+
// ---- Benchmark Reports ----
|
|
1180
|
+
insertBenchmarkReport(report) {
|
|
1181
|
+
this.db.prepare(`
|
|
1182
|
+
INSERT INTO benchmark_reports
|
|
1183
|
+
(run_id, suite_id, suite_version, project_root, commit_sha, task_count, status, report_json, created_at)
|
|
1184
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1185
|
+
ON CONFLICT(run_id) DO UPDATE SET
|
|
1186
|
+
suite_id = excluded.suite_id,
|
|
1187
|
+
suite_version = excluded.suite_version,
|
|
1188
|
+
project_root = excluded.project_root,
|
|
1189
|
+
commit_sha = excluded.commit_sha,
|
|
1190
|
+
task_count = excluded.task_count,
|
|
1191
|
+
status = excluded.status,
|
|
1192
|
+
report_json = excluded.report_json,
|
|
1193
|
+
created_at = excluded.created_at
|
|
1194
|
+
`).run(report.runId, report.suiteId, report.suiteVersion, report.projectRoot, report.commit, report.tasks.length, report.status, JSON.stringify(report), report.createdAt);
|
|
1195
|
+
}
|
|
1196
|
+
getBenchmarkReport(runId) {
|
|
1197
|
+
const row = this.db.prepare('SELECT report_json FROM benchmark_reports WHERE run_id = ?').get(runId);
|
|
1198
|
+
return row?.report_json
|
|
1199
|
+
? JSON.parse(row.report_json)
|
|
1200
|
+
: null;
|
|
1201
|
+
}
|
|
1202
|
+
getLatestBenchmarkReport(suiteId) {
|
|
1203
|
+
const row = (suiteId
|
|
1204
|
+
? this.db.prepare('SELECT report_json FROM benchmark_reports WHERE suite_id = ? ORDER BY created_at DESC LIMIT 1').get(suiteId)
|
|
1205
|
+
: this.db.prepare('SELECT report_json FROM benchmark_reports ORDER BY created_at DESC LIMIT 1').get());
|
|
1206
|
+
return row?.report_json
|
|
1207
|
+
? JSON.parse(row.report_json)
|
|
1208
|
+
: null;
|
|
1209
|
+
}
|
|
1210
|
+
listBenchmarkReports(limit = 50) {
|
|
1211
|
+
const rows = this.db.prepare(`
|
|
1212
|
+
SELECT run_id, suite_id, suite_version, project_root, commit_sha,
|
|
1213
|
+
task_count, status, created_at
|
|
1214
|
+
FROM benchmark_reports
|
|
1215
|
+
ORDER BY created_at DESC
|
|
1216
|
+
LIMIT ?
|
|
1217
|
+
`).all(Math.max(1, Math.min(limit, 500)));
|
|
1218
|
+
return rows.map((row) => ({
|
|
1219
|
+
runId: row.run_id,
|
|
1220
|
+
suiteId: row.suite_id,
|
|
1221
|
+
suiteVersion: row.suite_version,
|
|
1222
|
+
projectRoot: row.project_root,
|
|
1223
|
+
commit: row.commit_sha,
|
|
1224
|
+
taskCount: row.task_count,
|
|
1225
|
+
createdAt: row.created_at,
|
|
1226
|
+
status: row.status,
|
|
1227
|
+
}));
|
|
1228
|
+
}
|
|
1229
|
+
// ---- Enforcer Governance ----
|
|
1230
|
+
listGovernanceRules(includeDisabled = true) {
|
|
1231
|
+
const where = includeDisabled ? '' : 'WHERE enabled = 1';
|
|
1232
|
+
const rows = this.db.prepare(`
|
|
1233
|
+
SELECT id, name, gate, level, condition_json, action, message,
|
|
1234
|
+
enabled, created_at, updated_at
|
|
1235
|
+
FROM devflow_rules ${where}
|
|
1236
|
+
ORDER BY gate, id
|
|
1237
|
+
`).all();
|
|
1238
|
+
return rows.map(row => ({
|
|
1239
|
+
id: row.id,
|
|
1240
|
+
name: row.name,
|
|
1241
|
+
gate: row.gate,
|
|
1242
|
+
level: row.level,
|
|
1243
|
+
condition: parseJson(row.condition_json),
|
|
1244
|
+
action: row.action,
|
|
1245
|
+
message: row.message,
|
|
1246
|
+
enabled: row.enabled === 1,
|
|
1247
|
+
createdAt: row.created_at,
|
|
1248
|
+
updatedAt: row.updated_at,
|
|
1249
|
+
}));
|
|
1250
|
+
}
|
|
1251
|
+
getGovernanceRule(id) {
|
|
1252
|
+
return this.listGovernanceRules(true).find(rule => rule.id === id) ?? null;
|
|
1253
|
+
}
|
|
1254
|
+
upsertGovernanceRule(rule) {
|
|
1255
|
+
const now = Date.now();
|
|
1256
|
+
this.db.prepare(`
|
|
1257
|
+
INSERT INTO devflow_rules
|
|
1258
|
+
(id, name, gate, level, condition_json, action, message, enabled, created_at, updated_at)
|
|
1259
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1260
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
1261
|
+
name = excluded.name,
|
|
1262
|
+
gate = excluded.gate,
|
|
1263
|
+
level = excluded.level,
|
|
1264
|
+
condition_json = excluded.condition_json,
|
|
1265
|
+
action = excluded.action,
|
|
1266
|
+
message = excluded.message,
|
|
1267
|
+
enabled = excluded.enabled,
|
|
1268
|
+
updated_at = excluded.updated_at
|
|
1269
|
+
`).run(rule.id, rule.name, rule.gate, rule.level, JSON.stringify(rule.condition), rule.action, rule.message, rule.enabled ? 1 : 0, rule.createdAt ?? now, now);
|
|
1270
|
+
return this.getGovernanceRule(rule.id);
|
|
1271
|
+
}
|
|
1272
|
+
deleteGovernanceRule(id) {
|
|
1273
|
+
return this.db.prepare('DELETE FROM devflow_rules WHERE id = ?').run(id).changes > 0;
|
|
1274
|
+
}
|
|
1275
|
+
listGovernanceAudit(limit = 100, offset = 0) {
|
|
1276
|
+
const boundedLimit = Math.max(1, Math.min(limit, 500));
|
|
1277
|
+
const boundedOffset = Math.max(0, offset);
|
|
1278
|
+
const rows = this.db.prepare(`
|
|
1279
|
+
SELECT seq, ts, tool, args_hash, decision, rule_id, session_id, prev_hash, signature
|
|
1280
|
+
FROM devflow_audit_chain
|
|
1281
|
+
ORDER BY seq DESC
|
|
1282
|
+
LIMIT ? OFFSET ?
|
|
1283
|
+
`).all(boundedLimit, boundedOffset);
|
|
1284
|
+
const count = this.db.prepare('SELECT COUNT(*) AS total FROM devflow_audit_chain').get();
|
|
1285
|
+
return {
|
|
1286
|
+
items: rows.map(row => ({
|
|
1287
|
+
seq: row.seq,
|
|
1288
|
+
ts: row.ts,
|
|
1289
|
+
tool: row.tool,
|
|
1290
|
+
argsHash: row.args_hash,
|
|
1291
|
+
decision: row.decision,
|
|
1292
|
+
ruleId: row.rule_id,
|
|
1293
|
+
sessionId: row.session_id,
|
|
1294
|
+
prevHash: row.prev_hash,
|
|
1295
|
+
signature: row.signature,
|
|
1296
|
+
})),
|
|
1297
|
+
total: Number(count?.total ?? 0),
|
|
1298
|
+
};
|
|
1299
|
+
}
|
|
1300
|
+
// ---- Hook Lifecycle ----
|
|
1301
|
+
getHookReceipt(projectRoot) {
|
|
1302
|
+
const row = this.db.prepare(`
|
|
1303
|
+
SELECT project_root, last_mcp_call, bypass_count, updated_at
|
|
1304
|
+
FROM devflow_hook_receipts WHERE project_root = ?
|
|
1305
|
+
`).get(projectRoot);
|
|
1306
|
+
if (!row)
|
|
1307
|
+
return null;
|
|
1308
|
+
return {
|
|
1309
|
+
projectRoot: row.project_root,
|
|
1310
|
+
lastMcpCall: row.last_mcp_call ?? undefined,
|
|
1311
|
+
bypassCount: Number(row.bypass_count ?? 0),
|
|
1312
|
+
updatedAt: Number(row.updated_at),
|
|
1313
|
+
};
|
|
1314
|
+
}
|
|
1315
|
+
updateHookReceipt(projectRoot, updater) {
|
|
1316
|
+
this.db.exec('BEGIN IMMEDIATE');
|
|
1317
|
+
try {
|
|
1318
|
+
const current = this.getHookReceipt(projectRoot);
|
|
1319
|
+
const next = updater(current);
|
|
1320
|
+
const updatedAt = Date.now();
|
|
1321
|
+
this.db.prepare(`
|
|
1322
|
+
INSERT INTO devflow_hook_receipts
|
|
1323
|
+
(project_root, last_mcp_call, bypass_count, updated_at)
|
|
1324
|
+
VALUES (?, ?, ?, ?)
|
|
1325
|
+
ON CONFLICT(project_root) DO UPDATE SET
|
|
1326
|
+
last_mcp_call = excluded.last_mcp_call,
|
|
1327
|
+
bypass_count = excluded.bypass_count,
|
|
1328
|
+
updated_at = excluded.updated_at
|
|
1329
|
+
`).run(projectRoot, next.lastMcpCall ?? null, next.bypassCount ?? 0, updatedAt);
|
|
1330
|
+
this.db.exec('COMMIT');
|
|
1331
|
+
return {
|
|
1332
|
+
projectRoot,
|
|
1333
|
+
lastMcpCall: next.lastMcpCall,
|
|
1334
|
+
bypassCount: next.bypassCount ?? 0,
|
|
1335
|
+
updatedAt,
|
|
1336
|
+
};
|
|
1337
|
+
}
|
|
1338
|
+
catch (error) {
|
|
1339
|
+
try {
|
|
1340
|
+
this.db.exec('ROLLBACK');
|
|
1341
|
+
}
|
|
1342
|
+
catch { }
|
|
1343
|
+
throw error;
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
deleteHookReceipt(projectRoot) {
|
|
1347
|
+
return this.db.prepare('DELETE FROM devflow_hook_receipts WHERE project_root = ?')
|
|
1348
|
+
.run(projectRoot).changes > 0;
|
|
1349
|
+
}
|
|
1350
|
+
upsertContextReceipt(receipt) {
|
|
1351
|
+
this.db.prepare(`
|
|
1352
|
+
INSERT INTO devflow_context_receipts
|
|
1353
|
+
(project_root, session_id, execution_id, context_hash, issued_at, expires_at)
|
|
1354
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
1355
|
+
ON CONFLICT(project_root, session_id, execution_id) DO UPDATE SET
|
|
1356
|
+
context_hash = excluded.context_hash,
|
|
1357
|
+
issued_at = excluded.issued_at,
|
|
1358
|
+
expires_at = excluded.expires_at
|
|
1359
|
+
`).run(receipt.projectRoot, receipt.sessionId, receipt.executionId, receipt.contextHash, receipt.issuedAt, receipt.expiresAt);
|
|
1360
|
+
}
|
|
1361
|
+
getContextReceipt(projectRoot, sessionId, executionId) {
|
|
1362
|
+
const row = this.db.prepare(`
|
|
1363
|
+
SELECT project_root, session_id, execution_id, context_hash, issued_at, expires_at
|
|
1364
|
+
FROM devflow_context_receipts
|
|
1365
|
+
WHERE project_root = ? AND session_id = ? AND execution_id = ?
|
|
1366
|
+
`).get(projectRoot, sessionId, executionId);
|
|
1367
|
+
return row ? {
|
|
1368
|
+
projectRoot: row.project_root,
|
|
1369
|
+
sessionId: row.session_id,
|
|
1370
|
+
executionId: row.execution_id,
|
|
1371
|
+
contextHash: row.context_hash,
|
|
1372
|
+
issuedAt: row.issued_at,
|
|
1373
|
+
expiresAt: row.expires_at,
|
|
1374
|
+
} : null;
|
|
1375
|
+
}
|
|
1376
|
+
deleteContextReceipt(projectRoot, sessionId, executionId) {
|
|
1377
|
+
return executionId
|
|
1378
|
+
? this.db.prepare(`DELETE FROM devflow_context_receipts
|
|
1379
|
+
WHERE project_root = ? AND session_id = ? AND execution_id = ?`)
|
|
1380
|
+
.run(projectRoot, sessionId, executionId).changes
|
|
1381
|
+
: this.db.prepare(`DELETE FROM devflow_context_receipts
|
|
1382
|
+
WHERE project_root = ? AND session_id = ?`)
|
|
1383
|
+
.run(projectRoot, sessionId).changes;
|
|
1384
|
+
}
|
|
1385
|
+
purgeExpiredContextReceipts(now = Date.now()) {
|
|
1386
|
+
return this.db.prepare('DELETE FROM devflow_context_receipts WHERE expires_at <= ?')
|
|
1387
|
+
.run(now).changes;
|
|
1388
|
+
}
|
|
1389
|
+
recordMemoryDistillCheckpoint(checkpoint) {
|
|
1390
|
+
this.db.prepare(`
|
|
1391
|
+
INSERT INTO devflow_memory_distill_checkpoints
|
|
1392
|
+
(id, project_root, session_id, trigger, pending_events, released_leases, created_at)
|
|
1393
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
1394
|
+
`).run(checkpoint.id, checkpoint.projectRoot, checkpoint.sessionId ?? null, checkpoint.trigger, checkpoint.pendingEvents, checkpoint.releasedLeases, checkpoint.createdAt);
|
|
1395
|
+
}
|
|
1396
|
+
listMemoryDistillCheckpoints(projectRoot, limit = 50) {
|
|
1397
|
+
const rows = this.db.prepare(`
|
|
1398
|
+
SELECT id, project_root, session_id, trigger, pending_events, released_leases, created_at
|
|
1399
|
+
FROM devflow_memory_distill_checkpoints
|
|
1400
|
+
WHERE project_root = ?
|
|
1401
|
+
ORDER BY created_at DESC
|
|
1402
|
+
LIMIT ?
|
|
1403
|
+
`).all(projectRoot, Math.max(1, Math.min(limit, 500)));
|
|
1404
|
+
return rows.map(row => ({
|
|
1405
|
+
id: row.id,
|
|
1406
|
+
projectRoot: row.project_root,
|
|
1407
|
+
sessionId: row.session_id ?? undefined,
|
|
1408
|
+
trigger: row.trigger,
|
|
1409
|
+
pendingEvents: Number(row.pending_events),
|
|
1410
|
+
releasedLeases: Number(row.released_leases),
|
|
1411
|
+
createdAt: Number(row.created_at),
|
|
1412
|
+
}));
|
|
1413
|
+
}
|
|
833
1414
|
// Convenience methods for raw SQL queries (used by AutoChecker)
|
|
834
1415
|
all(sql, ...params) {
|
|
835
1416
|
return this.db.prepare(sql).all(...params);
|
|
@@ -842,3 +1423,54 @@ class DevFlowDatabase {
|
|
|
842
1423
|
}
|
|
843
1424
|
}
|
|
844
1425
|
exports.DevFlowDatabase = DevFlowDatabase;
|
|
1426
|
+
function parseJson(value) {
|
|
1427
|
+
if (typeof value !== 'string')
|
|
1428
|
+
return value;
|
|
1429
|
+
try {
|
|
1430
|
+
return JSON.parse(value);
|
|
1431
|
+
}
|
|
1432
|
+
catch {
|
|
1433
|
+
return value;
|
|
1434
|
+
}
|
|
1435
|
+
}
|
|
1436
|
+
function deriveQuery(input) {
|
|
1437
|
+
if (!input || typeof input !== 'object')
|
|
1438
|
+
return null;
|
|
1439
|
+
const record = input;
|
|
1440
|
+
for (const key of ['query', 'task', 'prompt', 'name', 'command']) {
|
|
1441
|
+
if (typeof record[key] === 'string' && record[key])
|
|
1442
|
+
return record[key];
|
|
1443
|
+
}
|
|
1444
|
+
return null;
|
|
1445
|
+
}
|
|
1446
|
+
function deriveEngine(toolName) {
|
|
1447
|
+
if (/project_context|symbol|dependency_graph|codegraph/.test(toolName))
|
|
1448
|
+
return 'codegraph';
|
|
1449
|
+
if (/knowledge|docs/.test(toolName))
|
|
1450
|
+
return 'knowledge';
|
|
1451
|
+
if (/memory/.test(toolName))
|
|
1452
|
+
return 'memory';
|
|
1453
|
+
return null;
|
|
1454
|
+
}
|
|
1455
|
+
function countTelemetryResults(value, depth = 0) {
|
|
1456
|
+
if (value == null || depth > 2)
|
|
1457
|
+
return 0;
|
|
1458
|
+
if (Array.isArray(value))
|
|
1459
|
+
return value.length;
|
|
1460
|
+
if (typeof value !== 'object')
|
|
1461
|
+
return value === '' ? 0 : 1;
|
|
1462
|
+
const record = value;
|
|
1463
|
+
const unwrapped = record.data ?? record.structuredContent;
|
|
1464
|
+
if (unwrapped !== undefined && unwrapped !== value)
|
|
1465
|
+
return countTelemetryResults(unwrapped, depth + 1);
|
|
1466
|
+
let count = 0;
|
|
1467
|
+
for (const key of ['files', 'results', 'result', 'memories', 'nodes', 'chunks', 'findings', 'symbols', 'keySymbols']) {
|
|
1468
|
+
if (Array.isArray(record[key]))
|
|
1469
|
+
count += record[key].length;
|
|
1470
|
+
}
|
|
1471
|
+
if (count > 0)
|
|
1472
|
+
return count;
|
|
1473
|
+
if (record.error)
|
|
1474
|
+
return 0;
|
|
1475
|
+
return Object.keys(record).length > 0 ? 1 : 0;
|
|
1476
|
+
}
|